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 / family-tree / index.js

index.js in Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor 2.0.7, at blocks/family-tree/index.js

305 lines 17.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 ( function () {
2 const el = window.wp.element.createElement;
3 const { registerBlockType } = window.wp.blocks;
4 const { InspectorControls, useBlockProps, PanelColorSettings } = window.wp.blockEditor;
5 const { PanelBody, RangeControl, ToggleControl, TextControl, SelectControl, Button, ColorPicker, Popover } = window.wp.components;
6 const { __ } = window.wp.i18n;
7 const { useState } = window.wp.element;
8
9 var _ftTC, _ftTV;
10 function _tc() { return _ftTC || (_ftTC = window.bkbgTypographyControl); }
11 function _tv(t, p) { return (_ftTV || (_ftTV = window.bkbgTypoCssVars)) ? _ftTV(t, p) : {}; }
12 var IP = function () { return window.bkbgIconPicker; };
13
14 // ── Layout engine ─────────────────────────────────────────────────────────
15 function calcLayout( persons, nodeW, nodeGap, levelGap, padY, svgW ) {
16 if ( !persons || !persons.length ) return {};
17 const pMap = {};
18 persons.forEach( p => pMap[p.id] = p );
19
20 // BFS to assign generation depth
21 const genMap = {};
22 const roots = persons.filter( p => !p.parentId );
23 const queue = roots.map( r => r.id );
24 roots.forEach( r => { genMap[r.id] = 0; } );
25
26 while ( queue.length ) {
27 const id = queue.shift();
28 const children = persons.filter( p => p.parentId === id );
29 children.forEach( c => {
30 if ( genMap[c.id] === undefined ) {
31 genMap[c.id] = genMap[id] + 1;
32 queue.push( c.id );
33 }
34 } );
35 }
36
37 // Spouses inherit their partner's generation
38 let changed = true;
39 while ( changed ) {
40 changed = false;
41 persons.forEach( p => {
42 if ( p.spouseId && pMap[p.spouseId] && genMap[p.spouseId] !== undefined && genMap[p.id] === undefined ) {
43 genMap[p.id] = genMap[p.spouseId];
44 changed = true;
45 }
46 } );
47 }
48 persons.forEach( p => { if ( genMap[p.id] === undefined ) genMap[p.id] = 0; } );
49
50 // Group by generation, keeping spouse pairs adjacent
51 const genGroups = {};
52 persons.forEach( p => {
53 const g = genMap[p.id];
54 if ( !genGroups[g] ) genGroups[g] = [];
55 genGroups[g].push( p );
56 } );
57
58 const ordered = {};
59 Object.keys( genGroups ).forEach( g => {
60 const seen = new Set();
61 const row = [];
62 genGroups[g].forEach( p => {
63 if ( seen.has( p.id ) ) return;
64 seen.add( p.id );
65 row.push( p );
66 if ( p.spouseId && pMap[p.spouseId] && genMap[p.spouseId] === Number( g ) && !seen.has( p.spouseId ) ) {
67 seen.add( p.spouseId );
68 row.push( pMap[p.spouseId] );
69 }
70 } );
71 ordered[g] = row;
72 } );
73
74 // Assign x/y positions
75 const posMap = {};
76 Object.keys( ordered ).forEach( g => {
77 const row = ordered[g];
78 const totalW = row.length * nodeW + ( row.length - 1 ) * nodeGap;
79 const startX = Math.max( 16, ( svgW - totalW ) / 2 );
80 row.forEach( ( p, i ) => {
81 posMap[p.id] = {
82 x: startX + i * ( nodeW + nodeGap ),
83 y: padY + Number( g ) * levelGap,
84 gen: Number( g ),
85 };
86 } );
87 } );
88
89 return posMap;
90 }
91
92 // ── SVG renderer ──────────────────────────────────────────────────────────
93 function renderFamilyTree( a ) {
94 const { svgWidth: W, nodeW, levelGap, nodeGap, padY } = a;
95 const CR = Math.round( nodeW * 0.28 ); // circle radius
96 const posMap = calcLayout( a.persons, nodeW, nodeGap, levelGap, padY, W );
97 const pMap = {};
98 a.persons.forEach( p => pMap[p.id] = p );
99
100 // Auto SVG height
101 const maxGen = Object.values( posMap ).reduce( ( m, p ) => Math.max( m, p.gen ), 0 );
102 const nodeHeight = CR * 2 + a.nameFontSize + a.yearFontSize + 16;
103 const H = padY + ( maxGen + 1 ) * levelGap + nodeHeight + 20;
104
105 const nodes = [];
106
107 // Background
108 nodes.push( el( 'rect', { key: 'bg', x: 0, y: 0, width: W, height: H, fill: a.bgColor || '#fafafa', rx: 10 } ) );
109
110 // ── Spouse connection lines (dashed horizontal) ──────────────────────
111 const drawnSpouses = new Set();
112 a.persons.forEach( p => {
113 if ( !p.spouseId || !posMap[p.id] || !posMap[p.spouseId] ) return;
114 const key = [ p.id, p.spouseId ].sort().join( '_' );
115 if ( drawnSpouses.has( key ) ) return;
116 drawnSpouses.add( key );
117
118 const p1 = posMap[p.id];
119 const p2 = posMap[p.spouseId];
120 const cx1 = p1.x + nodeW / 2;
121 const cx2 = p2.x + nodeW / 2;
122 const cy = p1.y + CR;
123 const x1 = Math.min( cx1, cx2 ) + CR + 4;
124 const x2 = Math.max( cx1, cx2 ) - CR - 4;
125 const mx = ( x1 + x2 ) / 2;
126
127 if ( x2 > x1 ) {
128 nodes.push( el( 'line', { key: 'spouse_' + key, x1, y1: cy, x2, y2: cy, stroke: a.lineColor, strokeWidth: 1.5, strokeDasharray: '4 3' } ) );
129 nodes.push( el( 'text', { key: 'heart_' + key, x: mx, y: cy + 1, textAnchor: 'middle', dominantBaseline: 'middle', fontSize: 11, fontFamily: 'inherit' }, '' ) );
130 }
131 } );
132
133 // ── Parent → child elbow lines ────────────────────────────────────────
134 a.persons.forEach( p => {
135 if ( !p.parentId || !posMap[p.id] || !posMap[p.parentId] ) return;
136 const par = posMap[p.parentId];
137 const chd = posMap[p.id];
138 const pcx = par.x + nodeW / 2;
139 const pcy = par.y + CR * 2 + 4;
140 const ccx = chd.x + nodeW / 2;
141 const ccy = chd.y - 4;
142 const midY = ( pcy + ccy ) / 2;
143
144 nodes.push( el( 'path', {
145 key: 'link_' + p.id,
146 d: `M ${ pcx } ${ pcy } V ${ midY } H ${ ccx } V ${ ccy }`,
147 stroke: a.lineColor, strokeWidth: 2, fill: 'none', strokeLinecap: 'round', strokeLinejoin: 'round',
148 } ) );
149 } );
150
151 // ── Person nodes ──────────────────────────────────────────────────────
152 a.persons.forEach( p => {
153 if ( !posMap[p.id] ) return;
154 const pos = posMap[p.id];
155 const cx = pos.x + nodeW / 2;
156 const cy = pos.y + CR;
157 const color = p.color || '#4f46e5';
158
159 nodes.push( el( 'circle', { key: 'pbg_' + p.id, cx, cy, r: CR, fill: color, opacity: 0.15 } ) );
160 nodes.push( el( 'circle', { key: 'pst_' + p.id, cx, cy, r: CR, fill: 'none', stroke: color, strokeWidth: 2.5 } ) );
161
162 // Emoji
163 nodes.push( el( 'text', {
164 key: 'em_' + p.id, x: cx, y: cy,
165 textAnchor: 'middle', dominantBaseline: 'middle',
166 fontSize: Math.max( 12, Math.floor( CR * 0.88 ) ), fontFamily: 'inherit',
167 }, p.emoji || '👤' ) );
168
169 // Name
170 nodes.push( el( 'text', {
171 key: 'nm_' + p.id, x: cx, y: pos.y + CR * 2 + 13,
172 textAnchor: 'middle', dominantBaseline: 'middle',
173 fill: a.textColor, fontSize: a.nameFontSize, fontWeight: a.nameFontWeight || 600, fontFamily: 'inherit',
174 }, p.name || '' ) );
175
176 // Years
177 if ( p.years ) {
178 nodes.push( el( 'text', {
179 key: 'yr_' + p.id, x: cx, y: pos.y + CR * 2 + 13 + a.nameFontSize + 3,
180 textAnchor: 'middle', dominantBaseline: 'middle',
181 fill: color, fontSize: a.yearFontSize, fontFamily: 'inherit', opacity: 0.8,
182 }, p.years ) );
183 }
184 } );
185
186 return el( 'svg', {
187 viewBox: `0 0 ${ W } ${ H }`, width: '100%',
188 style: { display: 'block', maxWidth: W + 'px', margin: '0 auto' },
189 }, ...nodes );
190 }
191
192 // ── Helpers ───────────────────────────────────────────────────────────────
193 let _nextId = 100;
194 function genId() { return String( ++_nextId ); }
195
196 function updPerson( setAttributes, persons, idx, field, val ) {
197 const next = persons.map( ( p, i ) => i === idx ? { ...p, [field]: val } : p );
198 setAttributes( { persons: next } );
199 }
200
201 // ── Block registration ──────────────────────────────────────────────────── /* ── colour-swatch + popover ── */
202 function BkbgColorSwatch(p) {
203 var st = useState(false), open = st[0], setOpen = st[1];
204 return el('div', { style:{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'4px 0', gap:'8px' } },
205 el('span', { style:{ fontSize:'12px', color:'#1e1e1e', flex:1, lineHeight:1.4 } }, p.label),
206 el('div', { style:{ position:'relative', flexShrink:0 } },
207 el('button', { type:'button', title: p.value||'none', onClick: function(){ setOpen(!open); },
208 style:{ width:'28px', height:'28px', borderRadius:'4px', border: open ? '2px solid #007cba' : '2px solid #ddd', cursor:'pointer', padding:0, display:'block', background: p.value||'#ffffff', flexShrink:0 } }),
209 open && el(Popover, { position:'bottom left', onClose: function(){ setOpen(false); } },
210 el('div', { style:{ padding:'8px' }, onMouseDown: function(e){ e.stopPropagation(); } },
211 el('div', { style:{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:'6px' } },
212 el('strong', { style:{ fontSize:'12px' } }, p.label),
213 el(Button, { icon:'no-alt', isSmall:true, onClick: function(){ setOpen(false); } })
214 ),
215 el(ColorPicker, { color: p.value, enableAlpha:true, onChange: p.onChange })
216 )
217 )
218 )
219 );
220 }
221 registerBlockType( 'blockenberg/family-tree', {
222 edit: function ( props ) {
223 const { attributes: a, setAttributes } = props;
224 const blockProps = useBlockProps( { className: 'bkbg-family-tree-wrap', style: Object.assign( {},
225 _tv( a.typoTitle || {}, '--bkbg-ft-ttl-' )
226 ) } );
227
228 const personOptions = [
229 { label: __( '— None —', 'blockenberg' ), value: '' },
230 ...a.persons.map( p => ( { label: p.name || `Person ${ p.id }`, value: p.id } ) ),
231 ];
232
233 return el( 'div', blockProps,
234 el( InspectorControls, {},
235
236 el( PanelBody, { title: __( 'Tree Settings', 'blockenberg' ), initialOpen: true },
237 el( TextControl, { label: __( 'Title', 'blockenberg' ), value: a.title, onChange: v => setAttributes( { title: v } ) } ),
238 el( ToggleControl, { label: __( 'Show Title', 'blockenberg' ), checked: a.showTitle, onChange: v => setAttributes( { showTitle: v } ) } ),
239 el( RangeControl, { label: __( 'Canvas Width', 'blockenberg' ), value: a.svgWidth, onChange: v => setAttributes( { svgWidth: v } ), min: 400, max: 1200, step: 20 } ),
240 el( RangeControl, { label: __( 'Node Width', 'blockenberg' ), value: a.nodeW, onChange: v => setAttributes( { nodeW: v } ), min: 70, max: 160 } ),
241 el( RangeControl, { label: __( 'Level Gap', 'blockenberg' ), value: a.levelGap, onChange: v => setAttributes( { levelGap: v } ), min: 100, max: 250 } ),
242 el( RangeControl, { label: __( 'Node Spacing', 'blockenberg' ), value: a.nodeGap, onChange: v => setAttributes( { nodeGap: v } ), min: 4, max: 60 } ),
243 ),
244
245
246 el( PanelBody, { title: __( 'Typography', 'blockenberg' ), initialOpen: false },
247 _tc() && el( _tc(), { label: 'Title', typo: a.typoTitle || {}, onChange: v => setAttributes( { typoTitle: v } ) } ),
248 el( RangeControl, { label: __( 'Name Font Size', 'blockenberg' ), value: a.nameFontSize, onChange: v => setAttributes( { nameFontSize: v } ), min: 8, max: 18 } ),
249 el( SelectControl, { label: __( 'Name Font Weight', 'blockenberg' ), value: a.nameFontWeight, onChange: v => setAttributes( { nameFontWeight: v } ), options: [ { label: 'Normal (400)', value: '400' }, { label: 'Medium (500)', value: '500' }, { label: 'Semi-Bold (600)', value: '600' }, { label: 'Bold (700)', value: '700' }, { label: 'Extra-Bold (800)', value: '800' } ] } ),
250 el( RangeControl, { label: __( 'Year Font Size', 'blockenberg' ), value: a.yearFontSize, onChange: v => setAttributes( { yearFontSize: v } ), min: 8, max: 16 } )
251 ),
252 el( PanelColorSettings, {
253 title: __( 'Colors', 'blockenberg' ),
254 initialOpen: false,
255 colorSettings: [
256 { label: __( 'Background', 'blockenberg' ), value: a.bgColor, onChange: v => setAttributes( { bgColor: v || '#fafafa' } ) },
257 { label: __( 'Line Color', 'blockenberg' ), value: a.lineColor, onChange: v => setAttributes( { lineColor: v || '#9ca3af' } ) },
258 { label: __( 'Title Color', 'blockenberg' ), value: a.titleColor, onChange: v => setAttributes( { titleColor: v || '#111827' } ) },
259 { label: __( 'Text Color', 'blockenberg' ), value: a.textColor, onChange: v => setAttributes( { textColor: v || '#111827' } ) },
260 ]
261 } ),
262
263 el( PanelBody, { title: __( 'People', 'blockenberg' ), initialOpen: false },
264 el( Button, {
265 variant: 'secondary', style: { marginBottom: 10 },
266 onClick: () => setAttributes( { persons: [ ...a.persons, { id: genId(), name: 'New Person', years: '', emoji: '🧑', parentId: '', spouseId: '', color: '#6b7280', emojiType: 'custom-char', emojiDashicon: '', emojiImageUrl: '' } ] } )
267 }, __( '+ Add Person', 'blockenberg' ) ),
268
269 a.persons.map( ( person, i ) =>
270 el( PanelBody, { key: person.id, title: person.name || `Person ${ i + 1 }`, initialOpen: false },
271 el( TextControl, { label: __( 'Name', 'blockenberg' ), value: person.name, onChange: v => updPerson( setAttributes, a.persons, i, 'name', v ) } ),
272 el( TextControl, { label: __( 'Years / Dates', 'blockenberg' ), value: person.years, onChange: v => updPerson( setAttributes, a.persons, i, 'years', v ) } ),
273 el(IP().IconPickerControl, {
274 iconType: person.emojiType, customChar: person.emoji, dashicon: person.emojiDashicon, imageUrl: person.emojiImageUrl,
275 onChangeType: v => updPerson( setAttributes, a.persons, i, 'emojiType', v ),
276 onChangeChar: v => updPerson( setAttributes, a.persons, i, 'emoji', v ),
277 onChangeDashicon: v => updPerson( setAttributes, a.persons, i, 'emojiDashicon', v ),
278 onChangeImageUrl: v => updPerson( setAttributes, a.persons, i, 'emojiImageUrl', v )
279 }),
280 el( BkbgColorSwatch, { label: __( 'Color', 'blockenberg' ), value: person.color, onChange: v => updPerson( setAttributes, a.persons, i, 'color', v ) } ),
281 el( SelectControl, { label: __( 'Parent (father/mother)', 'blockenberg' ), value: person.parentId, options: personOptions, onChange: v => updPerson( setAttributes, a.persons, i, 'parentId', v ) } ),
282 el( SelectControl, { label: __( 'Spouse / Partner', 'blockenberg' ), value: person.spouseId, options: personOptions, onChange: v => updPerson( setAttributes, a.persons, i, 'spouseId', v ) } ),
283 el( Button, { isDestructive: true, isSmall: true, style: { marginTop: 4 }, onClick: () => setAttributes( { persons: a.persons.filter( ( _, x ) => x !== i ) } ) }, __( 'Remove Person', 'blockenberg' ) ),
284 )
285 ),
286 ),
287 ),
288
289 a.showTitle && a.title && el( 'h3', { className: 'bkbg-ft-title', style: { color: a.titleColor } }, a.title ),
290 el( 'div', { className: 'bkbg-ft-svg' }, renderFamilyTree( a ) ),
291 );
292 },
293
294 save: function ( { attributes: a } ) {
295 const blockProps = useBlockProps.save( { className: 'bkbg-family-tree-wrap', style: Object.assign( {},
296 _tv( a.typoTitle || {}, '--bkbg-ft-ttl-' )
297 ) } );
298 return el( 'div', blockProps,
299 a.showTitle && a.title ? el( 'h3', { className: 'bkbg-ft-title', style: { color: a.titleColor } }, a.title ) : null,
300 el( 'div', { className: 'bkbg-ft-svg' }, renderFamilyTree( a ) ),
301 );
302 },
303 } );
304 }() );
305