PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.14
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.14
2.0.14 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 / flashcard / index.js

index.js in Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor 2.0.14, at blocks/flashcard/index.js

488 lines 26.1 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 useEffect = wp.element.useEffect;
4 var Fragment = wp.element.Fragment;
5 var registerBlockType = wp.blocks.registerBlockType;
6 var __ = wp.i18n.__;
7 var InspectorControls = wp.blockEditor.InspectorControls;
8 var PanelColorSettings = wp.blockEditor.PanelColorSettings;
9 var useBlockProps = wp.blockEditor.useBlockProps;
10 var PanelBody = wp.components.PanelBody;
11 var RangeControl = wp.components.RangeControl;
12 var TextControl = wp.components.TextControl;
13 var ToggleControl = wp.components.ToggleControl;
14
15 var _flTC, _flTV;
16 function _tc() { return _flTC || (_flTC = window.bkbgTypographyControl); }
17 function _tv() { return _flTV || (_flTV = window.bkbgTypoCssVars); }
18
19 var DEFAULT_CARDS = [
20 { id: '1', front: 'What is the capital of France?', back: 'Paris', known: false },
21 { id: '2', front: 'What is 7 × 8?', back: '56', known: false },
22 { id: '3', front: 'Who wrote Romeo and Juliet?', back: 'William Shakespeare', known: false },
23 { id: '4', front: 'What is H₂O?', back: 'Water', known: false },
24 { id: '5', front: 'What year did WWII end?', back: '1945', known: false }
25 ];
26
27 /**
28 * Cards used to be stored as a JSON string. Accept both shapes so older
29 * posts keep working and can migrate cleanly.
30 */
31 function normalizeCards( cards ) {
32 if ( Array.isArray( cards ) ) {
33 return cards;
34 }
35 if ( typeof cards === 'string' && cards ) {
36 try {
37 var parsed = JSON.parse( cards );
38 if ( Array.isArray( parsed ) ) {
39 return parsed;
40 }
41 } catch ( e ) { /* fall through */ }
42 }
43 return DEFAULT_CARDS.slice();
44 }
45
46 /**
47 * Only frontend-relevant block attributes — never dump Advanced (bkbg*)
48 * attrs or other editor-only noise into data-opts. A full JSON.stringify(a)
49 * breaks block validation whenever attributes are added or defaults change.
50 */
51 function buildFrontendOpts( a ) {
52 return {
53 title: a.title,
54 subtitle: a.subtitle,
55 showTitle: a.showTitle,
56 showSubtitle: a.showSubtitle,
57 cards: normalizeCards( a.cards ),
58 showProgress: a.showProgress,
59 showShuffleBtn: a.showShuffleBtn,
60 showKnownBtn: a.showKnownBtn,
61 enableKeyboardShortcuts: a.enableKeyboardShortcuts !== false,
62 showKeyboardHint: a.showKeyboardHint,
63 cardHeight: a.cardHeight,
64 accentColor: a.accentColor,
65 cardFrontBg: a.cardFrontBg,
66 cardBackBg: a.cardBackBg,
67 cardBackColor: a.cardBackColor,
68 knownColor: a.knownColor,
69 sectionBg: a.sectionBg,
70 cardBg: a.cardBg,
71 titleColor: a.titleColor,
72 subtitleColor: a.subtitleColor,
73 labelColor: a.labelColor,
74 titleFontSize: a.titleFontSize,
75 titleFontWeight: a.titleFontWeight,
76 titleLineHeight: a.titleLineHeight,
77 subtitleFontSize: a.subtitleFontSize,
78 subtitleFontWeight: a.subtitleFontWeight,
79 subtitleLineHeight: a.subtitleLineHeight,
80 cardFontSize: a.cardFontSize,
81 cardFontWeight: a.cardFontWeight,
82 cardLineHeight: a.cardLineHeight,
83 contentMaxWidth: a.contentMaxWidth,
84 typoTitle: a.typoTitle || {},
85 typoSubtitle: a.typoSubtitle || {},
86 typoCard: a.typoCard || {}
87 };
88 }
89
90 function saveElement( a ) {
91 return el( 'div', useBlockProps.save(),
92 el( 'div', {
93 className: 'bkbg-fc-app',
94 'data-opts': JSON.stringify( buildFrontendOpts( a ) )
95 } )
96 );
97 }
98
99 /**
100 * Legacy save: dumped every attribute (including Advanced bkbg* fields).
101 * Kept for deprecation matching / migrate path. Still fragile — new saves
102 * must never use this.
103 */
104 function legacySaveAllAttrs( props ) {
105 var a = props.attributes;
106 var opts = {};
107 Object.keys( a ).forEach( function ( key ) {
108 // Strip injected Advanced attrs so we can match older dumps that
109 // predate / postdate Advanced inconsistently less often.
110 if ( key.indexOf( 'bkbg' ) === 0 ) {
111 return;
112 }
113 opts[ key ] = a[ key ];
114 } );
115 // Legacy posts stored cards as a JSON string inside data-opts.
116 if ( Array.isArray( opts.cards ) ) {
117 opts.cards = JSON.stringify( opts.cards );
118 }
119 return el( 'div', useBlockProps.save(),
120 el( 'div', {
121 className: 'bkbg-fc-app',
122 'data-opts': JSON.stringify( opts )
123 } )
124 );
125 }
126
127 registerBlockType( 'blockenberg/flashcard', {
128 edit: function ( props ) {
129 var a = props.attributes;
130 var setAttributes = props.setAttributes;
131 var keyboardShortcutsEnabled = a.enableKeyboardShortcuts !== false;
132 var wrapStyle = Object.assign( {},
133 _tv()( a.typoTitle, '--bkbg-flash-tt-' ),
134 _tv()( a.typoSubtitle, '--bkbg-flash-ts-' ),
135 _tv()( a.typoCard, '--bkbg-flash-tc-' )
136 );
137 var blockProps = useBlockProps( { className: 'bkbg-fc-editor-wrap', style: wrapStyle } );
138
139 var cards = normalizeCards( a.cards );
140
141 // One-time migrate: string cards → array attribute.
142 useEffect( function () {
143 if ( typeof a.cards === 'string' ) {
144 setAttributes( { cards: normalizeCards( a.cards ) } );
145 }
146 }, [] );
147
148 var containerStyle = {
149 background: a.sectionBg,
150 borderRadius: '12px',
151 padding: '28px',
152 maxWidth: a.contentMaxWidth + 'px',
153 margin: '0 auto'
154 };
155
156 var cardPreviewStyle = {
157 background: a.cardFrontBg,
158 borderRadius: '12px',
159 height: a.cardHeight + 'px',
160 display: 'flex',
161 alignItems: 'center',
162 justifyContent: 'center',
163 textAlign: 'center',
164 padding: '24px',
165 boxShadow: '0 8px 32px rgba(99,102,241,0.15)',
166 color: a.labelColor,
167 cursor: 'pointer',
168 position: 'relative'
169 };
170
171 function addCard() {
172 setAttributes( {
173 cards: cards.concat( [ { id: Date.now().toString(), front: 'New question', back: 'Answer', known: false } ] )
174 } );
175 }
176
177 function removeCard( id ) {
178 setAttributes( { cards: cards.filter( function ( c ) { return c.id !== id; } ) } );
179 }
180
181 function updateCard( id, field, value ) {
182 var updated = cards.map( function ( c ) {
183 if ( c.id !== id ) return c;
184 var n = {};
185 Object.keys( c ).forEach( function ( k ) { n[ k ] = c[ k ]; } );
186 n[ field ] = value;
187 return n;
188 } );
189 setAttributes( { cards: updated } );
190 }
191
192 var TC = _tc();
193
194 return el(
195 Fragment,
196 null,
197 el(
198 InspectorControls,
199 null,
200 el(
201 PanelBody,
202 { title: __( 'Content', 'blockenberg' ), initialOpen: true },
203 el( TextControl, {
204 label: __( 'Title', 'blockenberg' ),
205 value: a.title,
206 onChange: function ( v ) { setAttributes( { title: v } ); }
207 } ),
208 el( TextControl, {
209 label: __( 'Subtitle', 'blockenberg' ),
210 value: a.subtitle,
211 onChange: function ( v ) { setAttributes( { subtitle: v } ); }
212 } ),
213 el( ToggleControl, {
214 label: __( 'Show Title', 'blockenberg' ),
215 checked: a.showTitle,
216 onChange: function ( v ) { setAttributes( { showTitle: v } ); },
217 __nextHasNoMarginBottom: true
218 } ),
219 el( ToggleControl, {
220 label: __( 'Show Subtitle', 'blockenberg' ),
221 checked: a.showSubtitle,
222 onChange: function ( v ) { setAttributes( { showSubtitle: v } ); },
223 __nextHasNoMarginBottom: true
224 } )
225 ),
226 el(
227 PanelBody,
228 { title: __( 'Cards', 'blockenberg' ), initialOpen: true },
229 cards.map( function ( card ) {
230 return el(
231 'div',
232 { key: card.id, style: { background: '#f9f9f9', borderRadius: '8px', padding: '12px', marginBottom: '10px', border: '1px solid #e5e7eb' } },
233 el( TextControl, {
234 label: __( 'Front', 'blockenberg' ),
235 value: card.front,
236 onChange: function ( v ) { updateCard( card.id, 'front', v ); }
237 } ),
238 el( TextControl, {
239 label: __( 'Back', 'blockenberg' ),
240 value: card.back,
241 onChange: function ( v ) { updateCard( card.id, 'back', v ); }
242 } ),
243 el(
244 'button',
245 {
246 style: { background: '#fee2e2', color: '#991b1b', border: 'none', borderRadius: '4px', padding: '4px 10px', cursor: 'pointer', fontSize: '12px' },
247 onClick: function () { removeCard( card.id ); }
248 },
249 __( 'Remove', 'blockenberg' )
250 )
251 );
252 } ),
253 el(
254 'button',
255 {
256 style: { background: a.accentColor, color: '#fff', border: 'none', borderRadius: '6px', padding: '8px 16px', cursor: 'pointer', width: '100%', fontWeight: '600' },
257 onClick: addCard
258 },
259 __( '+ Add Card', 'blockenberg' )
260 )
261 ),
262 el(
263 PanelBody,
264 { title: __( 'Options', 'blockenberg' ), initialOpen: false },
265 el( ToggleControl, {
266 label: __( 'Show Progress Bar', 'blockenberg' ),
267 checked: a.showProgress,
268 onChange: function ( v ) { setAttributes( { showProgress: v } ); },
269 __nextHasNoMarginBottom: true
270 } ),
271 el( ToggleControl, {
272 label: __( 'Show Shuffle Button', 'blockenberg' ),
273 checked: a.showShuffleBtn,
274 onChange: function ( v ) { setAttributes( { showShuffleBtn: v } ); },
275 __nextHasNoMarginBottom: true
276 } ),
277 el( ToggleControl, {
278 label: __( 'Show "Mark Known" Button', 'blockenberg' ),
279 checked: a.showKnownBtn,
280 onChange: function ( v ) { setAttributes( { showKnownBtn: v } ); },
281 __nextHasNoMarginBottom: true
282 } ),
283 el( ToggleControl, {
284 label: __( 'Enable keyboard shortcuts', 'blockenberg' ),
285 checked: keyboardShortcutsEnabled,
286 onChange: function ( v ) { setAttributes( { enableKeyboardShortcuts: v } ); },
287 __nextHasNoMarginBottom: true
288 } ),
289 el( ToggleControl, {
290 label: __( 'Show Keyboard Hint', 'blockenberg' ),
291 checked: a.showKeyboardHint,
292 onChange: function ( v ) { setAttributes( { showKeyboardHint: v } ); },
293 __nextHasNoMarginBottom: true
294 } ),
295 el( RangeControl, {
296 label: __( 'Card Height (px)', 'blockenberg' ),
297 value: a.cardHeight,
298 min: 120,
299 max: 500,
300 onChange: function ( v ) { setAttributes( { cardHeight: v } ); }
301 } )
302 ),
303 el(
304 PanelBody,
305 { title: __( 'Typography', 'blockenberg' ), initialOpen: false },
306 TC && el( TC, { label: __( 'Title', 'blockenberg' ), value: a.typoTitle || {}, onChange: function ( v ) { setAttributes( { typoTitle: v } ); } } ),
307 TC && el( TC, { label: __( 'Subtitle', 'blockenberg' ), value: a.typoSubtitle || {}, onChange: function ( v ) { setAttributes( { typoSubtitle: v } ); } } ),
308 TC && el( TC, { label: __( 'Card Text', 'blockenberg' ), value: a.typoCard || {}, onChange: function ( v ) { setAttributes( { typoCard: v } ); } } )
309 ),
310 el(
311 PanelBody,
312 { title: __( 'Appearance', 'blockenberg' ), initialOpen: false },
313 el( RangeControl, {
314 label: __( 'Max Width (px)', 'blockenberg' ),
315 value: a.contentMaxWidth,
316 min: 320,
317 max: 1200,
318 onChange: function ( v ) { setAttributes( { contentMaxWidth: v } ); }
319 } )
320 ),
321 el(
322 PanelColorSettings,
323 {
324 title: __( 'Colors', 'blockenberg' ),
325 initialOpen: false,
326 colorSettings: [
327 { value: a.accentColor, onChange: function ( v ) { setAttributes( { accentColor: v || '#6366f1' } ); }, label: __( 'Accent Color', 'blockenberg' ) },
328 { value: a.cardFrontBg, onChange: function ( v ) { setAttributes( { cardFrontBg: v || '#ffffff' } ); }, label: __( 'Card Front Background', 'blockenberg' ) },
329 { value: a.cardBackBg, onChange: function ( v ) { setAttributes( { cardBackBg: v || '#4f46e5' } ); }, label: __( 'Card Back Background', 'blockenberg' ) },
330 { value: a.cardBackColor, onChange: function ( v ) { setAttributes( { cardBackColor: v || '#ffffff' } ); }, label: __( 'Card Back Text', 'blockenberg' ) },
331 { value: a.knownColor, onChange: function ( v ) { setAttributes( { knownColor: v || '#22c55e' } ); }, label: __( 'Known Color', 'blockenberg' ) },
332 { value: a.sectionBg, onChange: function ( v ) { setAttributes( { sectionBg: v || '#f5f3ff' } ); }, label: __( 'Section Background', 'blockenberg' ) },
333 { value: a.cardBg, onChange: function ( v ) { setAttributes( { cardBg: v || '#ffffff' } ); }, label: __( 'Controls Background', 'blockenberg' ) },
334 { value: a.titleColor, onChange: function ( v ) { setAttributes( { titleColor: v || '#3730a3' } ); }, label: __( 'Title Color', 'blockenberg' ) },
335 { value: a.subtitleColor, onChange: function ( v ) { setAttributes( { subtitleColor: v || '#6b7280' } ); }, label: __( 'Subtitle Color', 'blockenberg' ) },
336 { value: a.labelColor, onChange: function ( v ) { setAttributes( { labelColor: v || '#374151' } ); }, label: __( 'Label Color', 'blockenberg' ) }
337 ]
338 }
339 )
340 ),
341 el(
342 'div',
343 blockProps,
344 el(
345 'div',
346 { style: containerStyle },
347 a.showTitle && el( 'div', {
348 className: 'bkbg-fc-title',
349 style: { color: a.titleColor, marginBottom: '6px' }
350 }, a.title ),
351 a.showSubtitle && el( 'div', {
352 className: 'bkbg-fc-subtitle',
353 style: { color: a.subtitleColor, marginBottom: '20px' }
354 }, a.subtitle ),
355 a.showProgress && el( 'div', {
356 style: { marginBottom: '16px' }
357 },
358 el( 'div', { style: { display: 'flex', justifyContent: 'space-between', fontSize: '13px', color: a.labelColor, marginBottom: '6px' } },
359 el( 'span', null, __( 'Progress', 'blockenberg' ) ),
360 el( 'span', null, '0 / ' + cards.length + ' known' )
361 ),
362 el( 'div', { style: { height: '8px', background: '#e5e7eb', borderRadius: '4px' } },
363 el( 'div', { style: { height: '100%', width: '0%', background: a.accentColor, borderRadius: '4px' } } )
364 )
365 ),
366 el( 'div', { className: 'bkbg-fc-front', style: cardPreviewStyle },
367 el( 'span', null, cards[ 0 ] ? cards[ 0 ].front : 'No cards yet' )
368 ),
369 el( 'div', { style: { display: 'flex', justifyContent: 'center', gap: '12px', marginTop: '16px', flexWrap: 'wrap' } },
370 el( 'button', { style: { background: '#e5e7eb', color: '#374151', border: 'none', borderRadius: '8px', padding: '10px 20px', cursor: 'pointer', fontSize: '20px' } }, '←' ),
371 a.showKnownBtn && el( 'button', { style: { background: a.knownColor, color: '#fff', border: 'none', borderRadius: '8px', padding: '10px 18px', cursor: 'pointer', fontWeight: '600' } }, '✓ Know it' ),
372 el( 'button', { style: { background: '#e5e7eb', color: '#374151', border: 'none', borderRadius: '8px', padding: '10px 20px', cursor: 'pointer', fontSize: '20px' } }, '→' )
373 ),
374 el( 'div', { style: { textAlign: 'center', marginTop: '10px', display: 'flex', gap: '10px', justifyContent: 'center', flexWrap: 'wrap' } },
375 el( 'span', { style: { color: a.subtitleColor, fontSize: '13px' } }, '1 / ' + cards.length ),
376 a.showShuffleBtn && el( 'button', { style: { background: 'transparent', border: '1px solid ' + a.accentColor, color: a.accentColor, borderRadius: '6px', padding: '4px 12px', cursor: 'pointer', fontSize: '12px' } }, '⇌ Shuffle' ),
377 el( 'button', { style: { background: 'transparent', border: '1px solid #e5e7eb', color: a.subtitleColor, borderRadius: '6px', padding: '4px 12px', cursor: 'pointer', fontSize: '12px' } }, '↺ Reset' )
378 ),
379 keyboardShortcutsEnabled && a.showKeyboardHint && el( 'div', { style: { textAlign: 'center', marginTop: '12px', color: a.subtitleColor, fontSize: '12px' } },
380 '← → Arrow keys to navigate • Space to flip'
381 )
382 )
383 )
384 );
385 },
386
387 save: function ( props ) {
388 return saveElement( props.attributes );
389 },
390
391 deprecated: [
392 {
393 attributes: {
394 title: { type: 'string', default: 'Flashcard Deck' },
395 subtitle: { type: 'string', default: 'Click a card to reveal the answer' },
396 showTitle: { type: 'boolean', default: true },
397 showSubtitle: { type: 'boolean', default: true },
398 cards: { type: 'string' },
399 showProgress: { type: 'boolean', default: true },
400 showShuffleBtn: { type: 'boolean', default: true },
401 showKnownBtn: { type: 'boolean', default: true },
402 enableKeyboardShortcuts: { type: 'boolean', default: true },
403 showKeyboardHint: { type: 'boolean', default: true },
404 cardHeight: { type: 'number', default: 220 },
405 accentColor: { type: 'string', default: '#6366f1' },
406 cardFrontBg: { type: 'string', default: '#ffffff' },
407 cardBackBg: { type: 'string', default: '#4f46e5' },
408 cardBackColor: { type: 'string', default: '#ffffff' },
409 knownColor: { type: 'string', default: '#22c55e' },
410 sectionBg: { type: 'string', default: '#f5f3ff' },
411 cardBg: { type: 'string', default: '#ffffff' },
412 titleColor: { type: 'string', default: '#3730a3' },
413 subtitleColor: { type: 'string', default: '#6b7280' },
414 labelColor: { type: 'string', default: '#374151' },
415 titleFontSize: { type: 'number', default: 26 },
416 titleFontWeight: { type: 'string', default: '700' },
417 titleLineHeight: { type: 'number', default: 1.2 },
418 subtitleFontSize: { type: 'number', default: 15 },
419 subtitleFontWeight: { type: 'string', default: '400' },
420 subtitleLineHeight: { type: 'number', default: 1.5 },
421 cardFontSize: { type: 'number', default: 18 },
422 cardFontWeight: { type: 'string', default: '500' },
423 cardLineHeight: { type: 'number', default: 1.5 },
424 contentMaxWidth: { type: 'number', default: 660 },
425 typoTitle: { type: 'object', default: {} },
426 typoSubtitle: { type: 'object', default: {} },
427 typoCard: { type: 'object', default: {} }
428 },
429 isEligible: function ( attributes ) {
430 return typeof attributes.cards === 'string';
431 },
432 migrate: function ( attributes ) {
433 var next = {};
434 Object.keys( attributes ).forEach( function ( k ) { next[ k ] = attributes[ k ]; } );
435 next.cards = normalizeCards( attributes.cards );
436 return next;
437 },
438 save: legacySaveAllAttrs
439 },
440 {
441 attributes: {
442 title: { type: 'string', default: 'Flashcard Deck' },
443 subtitle: { type: 'string', default: 'Click a card to reveal the answer' },
444 showTitle: { type: 'boolean', default: true },
445 showSubtitle: { type: 'boolean', default: true },
446 cards: { type: 'array' },
447 showProgress: { type: 'boolean', default: true },
448 showShuffleBtn: { type: 'boolean', default: true },
449 showKnownBtn: { type: 'boolean', default: true },
450 enableKeyboardShortcuts: { type: 'boolean', default: true },
451 showKeyboardHint: { type: 'boolean', default: true },
452 cardHeight: { type: 'number', default: 220 },
453 accentColor: { type: 'string', default: '#6366f1' },
454 cardFrontBg: { type: 'string', default: '#ffffff' },
455 cardBackBg: { type: 'string', default: '#4f46e5' },
456 cardBackColor: { type: 'string', default: '#ffffff' },
457 knownColor: { type: 'string', default: '#22c55e' },
458 sectionBg: { type: 'string', default: '#f5f3ff' },
459 cardBg: { type: 'string', default: '#ffffff' },
460 titleColor: { type: 'string', default: '#3730a3' },
461 subtitleColor: { type: 'string', default: '#6b7280' },
462 labelColor: { type: 'string', default: '#374151' },
463 titleFontSize: { type: 'number', default: 26 },
464 titleFontWeight: { type: 'string', default: '700' },
465 titleLineHeight: { type: 'number', default: 1.2 },
466 subtitleFontSize: { type: 'number', default: 15 },
467 subtitleFontWeight: { type: 'string', default: '400' },
468 subtitleLineHeight: { type: 'number', default: 1.5 },
469 cardFontSize: { type: 'number', default: 18 },
470 cardFontWeight: { type: 'string', default: '500' },
471 cardLineHeight: { type: 'number', default: 1.5 },
472 contentMaxWidth: { type: 'number', default: 660 },
473 typoTitle: { type: 'object', default: {} },
474 typoSubtitle: { type: 'object', default: {} },
475 typoCard: { type: 'object', default: {} }
476 },
477 migrate: function ( attributes ) {
478 var next = {};
479 Object.keys( attributes ).forEach( function ( k ) { next[ k ] = attributes[ k ]; } );
480 next.cards = normalizeCards( attributes.cards );
481 return next;
482 },
483 save: legacySaveAllAttrs
484 }
485 ]
486 } );
487 }() );
488