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

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

844 lines 41.5 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 __ = wp.i18n.__;
5 var useState = wp.element.useState;
6 var useMemo = wp.element.useMemo;
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 PanelColorSettings = wp.blockEditor.PanelColorSettings;
13 var ToggleControl = wp.components.ToggleControl;
14 var RangeControl = wp.components.RangeControl;
15 var SelectControl = wp.components.SelectControl;
16 var Button = wp.components.Button;
17 var BlockControls = wp.blockEditor.BlockControls;
18 var ToolbarGroup = wp.components.ToolbarGroup;
19 var ToolbarButton = wp.components.ToolbarButton;
20 var Popover = wp.components.Popover;
21 var TabPanel = wp.components.TabPanel;
22 var TextControl = wp.components.TextControl;
23
24 // Full Dashicons list (300+ icons)
25 var dashicons = [
26 // Admin Menu
27 'menu', 'admin-site', 'admin-site-alt', 'admin-site-alt2', 'admin-site-alt3', 'dashboard',
28 'admin-post', 'admin-media', 'admin-links', 'admin-page', 'admin-comments',
29 'admin-appearance', 'admin-plugins', 'admin-users', 'admin-tools', 'admin-settings',
30 'admin-network', 'admin-home', 'admin-generic', 'admin-collapse', 'filter',
31 'admin-customizer', 'admin-multisite',
32 // Welcome Screen
33 'welcome-write-blog', 'welcome-add-page', 'welcome-view-site', 'welcome-widgets-menus',
34 'welcome-comments', 'welcome-learn-more',
35 // Post Formats
36 'format-aside', 'format-image', 'format-gallery', 'format-video', 'format-status',
37 'format-quote', 'format-chat', 'format-audio',
38 // Media
39 'camera', 'camera-alt', 'images-alt', 'images-alt2', 'video-alt', 'video-alt2',
40 'video-alt3', 'media-archive', 'media-audio', 'media-code', 'media-default',
41 'media-document', 'media-interactive', 'media-spreadsheet', 'media-text',
42 'media-video', 'playlist-audio', 'playlist-video', 'controls-play',
43 'controls-pause', 'controls-forward', 'controls-skipforward', 'controls-back',
44 'controls-skipback', 'controls-repeat', 'controls-volumeon', 'controls-volumeoff',
45 // Image Editing
46 'image-crop', 'image-rotate', 'image-rotate-left', 'image-rotate-right',
47 'image-flip-vertical', 'image-flip-horizontal', 'image-filter',
48 // Sorting & Misc UI
49 'align-left', 'align-right', 'align-center', 'align-none', 'align-pull-left', 'align-pull-right',
50 'align-full-width', 'align-wide',
51 'lock', 'unlock', 'calendar', 'calendar-alt', 'visibility', 'hidden',
52 'post-status', 'edit', 'trash', 'sticky',
53 // Arrows & Navigation
54 'arrow-up', 'arrow-down', 'arrow-right', 'arrow-left',
55 'arrow-up-alt', 'arrow-down-alt', 'arrow-right-alt', 'arrow-left-alt',
56 'arrow-up-alt2', 'arrow-down-alt2', 'arrow-right-alt2', 'arrow-left-alt2',
57 'sort', 'leftright', 'randomize', 'list-view', 'excerpt-view', 'grid-view',
58 'move', 'undo', 'redo',
59 // Social & Sharing
60 'share', 'share-alt', 'share-alt2', 'rss', 'email', 'email-alt', 'email-alt2',
61 'networking', 'amazon', 'facebook', 'facebook-alt', 'google',
62 'twitter', 'twitter-alt', 'instagram', 'linkedin', 'pinterest', 'podio',
63 'reddit', 'spotify', 'twitch', 'whatsapp', 'xing', 'youtube',
64 // WordPress.org Specific
65 'hammer', 'art', 'migrate', 'performance', 'universal-access',
66 'universal-access-alt', 'tickets', 'nametag', 'clipboard', 'heart', 'megaphone',
67 'schedule', 'tide', 'rest-api', 'code-standards',
68 // BuddyPress
69 'buddicons-activity', 'buddicons-bbpress-logo', 'buddicons-buddypress-logo',
70 'buddicons-community', 'buddicons-forums', 'buddicons-friends',
71 'buddicons-groups', 'buddicons-pm', 'buddicons-replies',
72 'buddicons-topics', 'buddicons-tracking',
73 // WordPress Core
74 'wordpress', 'wordpress-alt', 'pressthis', 'update', 'update-alt',
75 'screenoptions', 'info', 'cart', 'feedback', 'cloud', 'cloud-saved',
76 'cloud-upload', 'translation', 'tag', 'category', 'archive',
77 // Status & Alerts
78 'yes', 'yes-alt', 'no', 'no-alt', 'plus', 'plus-alt', 'plus-alt2',
79 'minus', 'dismiss', 'marker', 'star-filled', 'star-half', 'star-empty',
80 'flag', 'warning', 'thumbs-up', 'thumbs-down', 'smiley',
81 // Location & Places
82 'location', 'location-alt', 'vault', 'shield', 'shield-alt',
83 'sos', 'search', 'slides', 'text-page', 'analytics', 'chart-pie',
84 'chart-bar', 'chart-line', 'chart-area', 'groups', 'businessman', 'businesswoman',
85 'businessperson', 'id', 'id-alt', 'products', 'awards', 'forms', 'testimonial',
86 'portfolio', 'book', 'book-alt', 'download', 'upload', 'backup',
87 'clock', 'lightbulb', 'microphone', 'desktop', 'laptop', 'tablet',
88 'smartphone', 'phone', 'index-card', 'carrot', 'building', 'store',
89 'album', 'palmtree', 'tickets-alt', 'money', 'money-alt', 'bank',
90 // Misc Objects
91 'layout', 'paperclip', 'color-picker', 'edit-large', 'edit-page',
92 'airplane', 'beer', 'bell', 'bitcoin', 'calculator', 'car', 'coffee',
93 'database', 'database-add', 'database-export', 'database-import',
94 'database-remove', 'database-view', 'drumstick', 'ellipsis', 'exit',
95 'food', 'fullscreen-alt', 'fullscreen-exit-alt', 'games', 'hourglass',
96 'html', 'insert', 'insert-after', 'insert-before', 'open-folder',
97 'pdf', 'pets', 'plugins-checked', 'printer', 'privacy', 'remove',
98 'saved', 'shortcode', 'superhero', 'superhero-alt', 'text', 'external',
99 // Table Operations
100 'table-col-after', 'table-col-before', 'table-col-delete',
101 'table-row-after', 'table-row-before', 'table-row-delete',
102 // Editor Controls
103 'editor-aligncenter', 'editor-alignleft', 'editor-alignright', 'editor-bold',
104 'editor-break', 'editor-code', 'editor-contract', 'editor-customchar',
105 'editor-expand', 'editor-help', 'editor-indent', 'editor-insertmore',
106 'editor-italic', 'editor-justify', 'editor-kitchensink', 'editor-ltr',
107 'editor-ol', 'editor-ol-rtl', 'editor-outdent', 'editor-paragraph',
108 'editor-paste-text', 'editor-paste-word', 'editor-quote', 'editor-removeformatting',
109 'editor-rtl', 'editor-spellcheck', 'editor-strikethrough', 'editor-table',
110 'editor-textcolor', 'editor-ul', 'editor-underline', 'editor-unlink', 'editor-video'
111 ];
112
113 // Helper: parse icon value (supports legacy string or new object format)
114 function parseIcon(iconValue) {
115 if (!iconValue) return { type: 'dashicon', value: 'flag' };
116 if (typeof iconValue === 'string') {
117 // Legacy format - just the icon name
118 return { type: 'dashicon', value: iconValue };
119 }
120 return iconValue;
121 }
122
123 registerBlockType('blockenberg/timeline', {
124 title: __('Timeline', 'blockenberg'),
125 icon: 'backup',
126 category: 'blockenberg',
127 description: __('Display events, milestones, or steps in a beautiful timeline layout.', 'blockenberg'),
128
129 edit: function (props) {
130 var attributes = props.attributes;
131 var setAttributes = props.setAttributes;
132 var isSelected = props.isSelected;
133 var a = attributes;
134
135 var hoverState = useState(null);
136 var hoveredItem = hoverState[0];
137 var setHoveredItem = hoverState[1];
138
139 // State for icon picker popover
140 var iconPickerState = useState(null);
141 var openIconPicker = iconPickerState[0];
142 var setOpenIconPicker = iconPickerState[1];
143
144 // Search state for dashicons
145 var searchState = useState('');
146 var iconSearch = searchState[0];
147 var setIconSearch = searchState[1];
148
149 // Filter dashicons based on search
150 var filteredDashicons = useMemo(function() {
151 if (!iconSearch) return dashicons;
152 var search = iconSearch.toLowerCase();
153 return dashicons.filter(function(icon) {
154 return icon.toLowerCase().includes(search);
155 });
156 }, [iconSearch]);
157
158 // Update item field
159 function updateItem(index, field, value) {
160 var newItems = a.items.map(function (item, i) {
161 if (i === index) {
162 var updated = {};
163 for (var k in item) { updated[k] = item[k]; }
164 updated[field] = value;
165 return updated;
166 }
167 return item;
168 });
169 setAttributes({ items: newItems });
170 }
171
172 // Add new item
173 function addItem() {
174 var newItem = {
175 date: 'New Date',
176 title: 'New Event',
177 content: 'Describe this milestone or event.',
178 icon: { type: 'dashicon', value: 'flag' }
179 };
180 setAttributes({ items: a.items.concat([newItem]) });
181 }
182
183 // Insert item after specific index
184 function insertItemAfter(index) {
185 var newItem = {
186 date: 'New Date',
187 title: 'New Event',
188 content: 'Describe this milestone or event.',
189 icon: { type: 'dashicon', value: 'flag' }
190 };
191 var newItems = a.items.slice();
192 newItems.splice(index + 1, 0, newItem);
193 setAttributes({ items: newItems });
194 }
195
196 // Remove item
197 function removeItem(index) {
198 if (a.items.length <= 1) return;
199 setAttributes({ items: a.items.filter(function (_, i) { return i !== index; }) });
200 }
201
202 // Move item
203 function moveItem(index, direction) {
204 var newIndex = index + direction;
205 if (newIndex < 0 || newIndex >= a.items.length) return;
206 var newItems = a.items.slice();
207 var temp = newItems[index];
208 newItems[index] = newItems[newIndex];
209 newItems[newIndex] = temp;
210 setAttributes({ items: newItems });
211 }
212
213 // Duplicate item
214 function duplicateItem(index) {
215 var newItems = a.items.slice();
216 var copy = Object.assign({}, a.items[index]);
217 newItems.splice(index + 1, 0, copy);
218 setAttributes({ items: newItems });
219 }
220
221 // Inspector controls
222 var inspector = el(InspectorControls, {},
223 // Layout Panel
224 el(PanelBody, { title: __('Layout', 'blockenberg'), initialOpen: true },
225 el(SelectControl, {
226 label: __('Layout Style', 'blockenberg'),
227 value: a.layoutStyle,
228 options: [
229 { label: __('Vertical', 'blockenberg'), value: 'vertical' },
230 { label: __('Horizontal', 'blockenberg'), value: 'horizontal' }
231 ],
232 onChange: function (v) { setAttributes({ layoutStyle: v }); }
233 }),
234 a.layoutStyle === 'vertical' && el(ToggleControl, {
235 label: __('Alternating Layout', 'blockenberg'),
236 checked: a.alternating,
237 __nextHasNoMarginBottom: true,
238 onChange: function (v) { setAttributes({ alternating: v }); }
239 }),
240 el(RangeControl, {
241 label: __('Item Spacing', 'blockenberg'),
242 value: a.itemSpacing,
243 onChange: function (v) { setAttributes({ itemSpacing: v }); },
244 min: 10,
245 max: 100
246 })
247 ),
248
249 // Connector Panel
250 el(PanelBody, { title: __('Connector Line', 'blockenberg'), initialOpen: false },
251 el(ToggleControl, {
252 label: __('Show Connector', 'blockenberg'),
253 checked: a.showConnector,
254 __nextHasNoMarginBottom: true,
255 onChange: function (v) { setAttributes({ showConnector: v }); }
256 }),
257 a.showConnector && el(SelectControl, {
258 label: __('Connector Style', 'blockenberg'),
259 value: a.connectorStyle,
260 options: [
261 { label: __('Solid', 'blockenberg'), value: 'solid' },
262 { label: __('Dashed', 'blockenberg'), value: 'dashed' },
263 { label: __('Dotted', 'blockenberg'), value: 'dotted' }
264 ],
265 onChange: function (v) { setAttributes({ connectorStyle: v }); }
266 }),
267 a.showConnector && el(RangeControl, {
268 label: __('Line Width', 'blockenberg'),
269 value: a.lineWidth,
270 onChange: function (v) { setAttributes({ lineWidth: v }); },
271 min: 1,
272 max: 8
273 })
274 ),
275
276 // Marker Panel
277 el(PanelBody, { title: __('Marker', 'blockenberg'), initialOpen: false },
278 el(SelectControl, {
279 label: __('Marker Style', 'blockenberg'),
280 value: a.markerStyle,
281 options: [
282 { label: __('Icon', 'blockenberg'), value: 'icon' },
283 { label: __('Number', 'blockenberg'), value: 'number' },
284 { label: __('Dot', 'blockenberg'), value: 'dot' }
285 ],
286 onChange: function (v) { setAttributes({ markerStyle: v }); }
287 }),
288 el(RangeControl, {
289 label: __('Marker Size', 'blockenberg'),
290 value: a.markerSize,
291 onChange: function (v) { setAttributes({ markerSize: v }); },
292 min: 24,
293 max: 80
294 })
295 ),
296
297 // Card Panel
298 el(PanelBody, { title: __('Card Style', 'blockenberg'), initialOpen: false },
299 el(ToggleControl, {
300 label: __('Card Shadow', 'blockenberg'),
301 checked: a.cardShadow,
302 __nextHasNoMarginBottom: true,
303 onChange: function (v) { setAttributes({ cardShadow: v }); }
304 }),
305 el(RangeControl, {
306 label: __('Card Padding', 'blockenberg'),
307 value: a.cardPadding,
308 onChange: function (v) { setAttributes({ cardPadding: v }); },
309 min: 12,
310 max: 48
311 }),
312 el(RangeControl, {
313 label: __('Border Radius', 'blockenberg'),
314 value: a.cardBorderRadius,
315 onChange: function (v) { setAttributes({ cardBorderRadius: v }); },
316 min: 0,
317 max: 24
318 }),
319 el(RangeControl, {
320 label: __('Border Width', 'blockenberg'),
321 value: a.cardBorderWidth,
322 onChange: function (v) { setAttributes({ cardBorderWidth: v }); },
323 min: 0,
324 max: 4
325 })
326 ),
327
328 // Animation Panel
329 el(PanelBody, { title: __('Animation', 'blockenberg'), initialOpen: false },
330 el(ToggleControl, {
331 label: __('Animate on Scroll', 'blockenberg'),
332 checked: a.animateOnScroll,
333 __nextHasNoMarginBottom: true,
334 onChange: function (v) { setAttributes({ animateOnScroll: v }); }
335 }),
336 a.animateOnScroll && el(SelectControl, {
337 label: __('Animation Type', 'blockenberg'),
338 value: a.animationType,
339 options: [
340 { label: __('Fade Up', 'blockenberg'), value: 'fade-up' },
341 { label: __('Fade In', 'blockenberg'), value: 'fade-in' },
342 { label: __('Slide In', 'blockenberg'), value: 'slide-in' },
343 { label: __('Scale Up', 'blockenberg'), value: 'scale-up' }
344 ],
345 onChange: function (v) { setAttributes({ animationType: v }); }
346 })
347 ),
348
349 // Typography Panel
350 el(PanelBody, { title: __('Typography', 'blockenberg'), initialOpen: false },
351 el('p', { style: { marginBottom: '8px', fontWeight: '600' } }, __('Date', 'blockenberg')),
352 el(RangeControl, {
353 label: __('Font Size', 'blockenberg'),
354 value: a.dateFontSize,
355 onChange: function (v) { setAttributes({ dateFontSize: v }); },
356 min: 10,
357 max: 24
358 }),
359 el(SelectControl, {
360 label: __('Font Weight', 'blockenberg'),
361 value: a.dateFontWeight,
362 options: [
363 { label: '400', value: '400' },
364 { label: '500', value: '500' },
365 { label: '600', value: '600' },
366 { label: '700', value: '700' }
367 ],
368 onChange: function (v) { setAttributes({ dateFontWeight: v }); }
369 }),
370 el('hr'),
371 el('p', { style: { marginBottom: '8px', fontWeight: '600' } }, __('Title', 'blockenberg')),
372 el(RangeControl, {
373 label: __('Font Size', 'blockenberg'),
374 value: a.titleFontSize,
375 onChange: function (v) { setAttributes({ titleFontSize: v }); },
376 min: 14,
377 max: 36
378 }),
379 el(SelectControl, {
380 label: __('Font Weight', 'blockenberg'),
381 value: a.titleFontWeight,
382 options: [
383 { label: '400', value: '400' },
384 { label: '500', value: '500' },
385 { label: '600', value: '600' },
386 { label: '700', value: '700' }
387 ],
388 onChange: function (v) { setAttributes({ titleFontWeight: v }); }
389 }),
390 el('hr'),
391 el('p', { style: { marginBottom: '8px', fontWeight: '600' } }, __('Content', 'blockenberg')),
392 el(RangeControl, {
393 label: __('Font Size', 'blockenberg'),
394 value: a.contentFontSize,
395 onChange: function (v) { setAttributes({ contentFontSize: v }); },
396 min: 12,
397 max: 24
398 })
399 ),
400
401 // Colors Panel
402 el(PanelColorSettings, {
403 title: __('Colors', 'blockenberg'),
404 initialOpen: false,
405 colorSettings: [
406 {
407 value: a.lineColor,
408 onChange: function (v) { setAttributes({ lineColor: v }); },
409 label: __('Line Color', 'blockenberg')
410 },
411 {
412 value: a.markerBg,
413 onChange: function (v) { setAttributes({ markerBg: v }); },
414 label: __('Marker Background', 'blockenberg')
415 },
416 {
417 value: a.markerColor,
418 onChange: function (v) { setAttributes({ markerColor: v }); },
419 label: __('Marker Icon Color', 'blockenberg')
420 },
421 {
422 value: a.cardBg,
423 onChange: function (v) { setAttributes({ cardBg: v }); },
424 label: __('Card Background', 'blockenberg')
425 },
426 {
427 value: a.cardBorderColor,
428 onChange: function (v) { setAttributes({ cardBorderColor: v }); },
429 label: __('Card Border Color', 'blockenberg')
430 },
431 {
432 value: a.dateColor,
433 onChange: function (v) { setAttributes({ dateColor: v }); },
434 label: __('Date Color', 'blockenberg')
435 },
436 {
437 value: a.titleColor,
438 onChange: function (v) { setAttributes({ titleColor: v }); },
439 label: __('Title Color', 'blockenberg')
440 },
441 {
442 value: a.contentColor,
443 onChange: function (v) { setAttributes({ contentColor: v }); },
444 label: __('Content Color', 'blockenberg')
445 }
446 ]
447 })
448 );
449
450 // CSS Variables
451 var wrapStyle = {
452 '--bkbg-tl-line-color': a.lineColor,
453 '--bkbg-tl-line-width': a.lineWidth + 'px',
454 '--bkbg-tl-marker-bg': a.markerBg,
455 '--bkbg-tl-marker-color': a.markerColor,
456 '--bkbg-tl-marker-size': a.markerSize + 'px',
457 '--bkbg-tl-card-bg': a.cardBg,
458 '--bkbg-tl-card-border': a.cardBorderColor,
459 '--bkbg-tl-card-border-w': a.cardBorderWidth + 'px',
460 '--bkbg-tl-card-radius': a.cardBorderRadius + 'px',
461 '--bkbg-tl-card-padding': a.cardPadding + 'px',
462 '--bkbg-tl-date-color': a.dateColor,
463 '--bkbg-tl-date-size': a.dateFontSize + 'px',
464 '--bkbg-tl-date-weight': a.dateFontWeight,
465 '--bkbg-tl-title-color': a.titleColor,
466 '--bkbg-tl-title-size': a.titleFontSize + 'px',
467 '--bkbg-tl-title-weight': a.titleFontWeight,
468 '--bkbg-tl-content-color': a.contentColor,
469 '--bkbg-tl-content-size': a.contentFontSize + 'px',
470 '--bkbg-tl-spacing': a.itemSpacing + 'px'
471 };
472
473 // Render full icon picker popover with tabs
474 function renderIconPicker(index) {
475 var currentIcon = parseIcon(a.items[index].icon);
476 var stop = function (e) {
477 if (e && e.stopPropagation) e.stopPropagation();
478 };
479
480 return el(Popover, {
481 position: 'bottom center',
482 className: 'bkbg-tl-icon-popover',
483 onClose: function () {
484 setOpenIconPicker(null);
485 setIconSearch('');
486 }
487 },
488 el('div', { className: 'bkbg-tl-icon-picker-modal', onMouseDown: stop, onClick: stop },
489 el('div', { className: 'bkbg-tl-icon-picker-header' },
490 el('h3', {}, __('Choose Icon', 'blockenberg')),
491 el(Button, {
492 icon: 'no-alt',
493 onClick: function() {
494 setOpenIconPicker(null);
495 setIconSearch('');
496 },
497 className: 'bkbg-tl-icon-picker-close'
498 })
499 ),
500 el(TabPanel, {
501 className: 'bkbg-tl-icon-tabs',
502 activeClass: 'is-active',
503 tabs: [
504 { name: 'dashicons', title: __('Dashicons', 'blockenberg'), className: 'bkbg-tl-tab-dashicons' },
505 { name: 'media', title: __('Media Library', 'blockenberg'), className: 'bkbg-tl-tab-media' }
506 ]
507 }, function(tab) {
508 if (tab.name === 'dashicons') {
509 return el('div', { className: 'bkbg-tl-dashicons-panel' },
510 // Search box
511 el(TextControl, {
512 placeholder: __('Search icons...', 'blockenberg'),
513 value: iconSearch,
514 onChange: setIconSearch,
515 className: 'bkbg-tl-icon-search'
516 }),
517 // Icons grid
518 el('div', { className: 'bkbg-tl-dashicons-grid' },
519 filteredDashicons.length === 0
520 ? el('p', { className: 'bkbg-tl-no-icons' }, __('No icons found', 'blockenberg'))
521 : filteredDashicons.map(function(icon) {
522 var isActive = currentIcon.type === 'dashicon' && currentIcon.value === icon;
523 return el('button', {
524 key: icon,
525 type: 'button',
526 className: 'bkbg-tl-dashicon-btn' + (isActive ? ' is-active' : ''),
527 onClick: function() {
528 updateItem(index, 'icon', { type: 'dashicon', value: icon });
529 setOpenIconPicker(null);
530 setIconSearch('');
531 },
532 title: icon
533 },
534 el('span', { className: 'dashicons dashicons-' + icon })
535 );
536 })
537 )
538 );
539 } else {
540 // Media Library tab - use wp.media directly for reliability
541 var openMediaLibrary = function() {
542 var frame = wp.media({
543 title: __('Select Icon Image', 'blockenberg'),
544 button: { text: __('Use as Icon', 'blockenberg') },
545 multiple: false,
546 library: { type: 'image' }
547 });
548 frame.on('select', function() {
549 var attachment = frame.state().get('selection').first().toJSON();
550 updateItem(index, 'icon', { type: 'custom', value: attachment.url });
551 setOpenIconPicker(null);
552 });
553 frame.open();
554 };
555
556 return el('div', { className: 'bkbg-tl-media-panel' },
557 // Current custom icon preview
558 currentIcon.type === 'custom' && el('div', { className: 'bkbg-tl-current-media' },
559 el('img', { src: currentIcon.value, alt: '' }),
560 el(Button, {
561 isDestructive: true,
562 variant: 'secondary',
563 onClick: function() {
564 updateItem(index, 'icon', { type: 'dashicon', value: 'flag' });
565 }
566 }, __('Remove', 'blockenberg'))
567 ),
568 // Media upload button using wp.media directly
569 el(Button, {
570 variant: 'secondary',
571 onClick: openMediaLibrary,
572 className: 'bkbg-tl-media-btn'
573 },
574 el('span', { className: 'dashicons dashicons-upload' }),
575 ' ',
576 __('Select from Media Library', 'blockenberg')
577 ),
578 el('p', { className: 'bkbg-tl-media-hint' },
579 __('Upload or select an image to use as a custom marker icon.', 'blockenberg')
580 )
581 );
582 }
583 })
584 )
585 );
586 }
587
588 // Render marker content (icon, number, or dot)
589 function renderMarkerContent(item, index) {
590 if (a.markerStyle === 'number') {
591 return el('span', { className: 'bkbg-tl-marker-number' }, index + 1);
592 } else if (a.markerStyle === 'dot') {
593 return el('span', { className: 'bkbg-tl-marker-dot' });
594 } else {
595 var iconData = parseIcon(item.icon);
596 if (iconData.type === 'custom') {
597 return el('img', {
598 src: iconData.value,
599 alt: '',
600 className: 'bkbg-tl-marker-img'
601 });
602 } else {
603 // Dashicon
604 return el('span', {
605 className: 'dashicons dashicons-' + iconData.value + ' bkbg-tl-marker-dashicon'
606 });
607 }
608 }
609 }
610
611 // Render items
612 var itemsEl = a.items.map(function (item, index) {
613 var isHovered = hoveredItem === index;
614 var itemClass = 'bkbg-tl-item';
615 if (a.alternating && a.layoutStyle === 'vertical') {
616 itemClass += index % 2 === 0 ? ' bkbg-tl-item-left' : ' bkbg-tl-item-right';
617 }
618 if (isHovered) itemClass += ' bkbg-tl-item-hovered';
619
620 // Marker is clickable when icon style
621 var markerProps = {
622 className: 'bkbg-tl-marker' + (a.markerStyle === 'icon' ? ' bkbg-tl-marker-clickable' : '')
623 };
624 if (a.markerStyle === 'icon') {
625 markerProps.onClick = function (e) {
626 e.stopPropagation();
627 setOpenIconPicker(openIconPicker === index ? null : index);
628 };
629 markerProps.title = __('Click to change icon', 'blockenberg');
630 }
631
632 return el('div', {
633 key: index,
634 className: itemClass,
635 onMouseEnter: function () { setHoveredItem(index); },
636 onMouseLeave: function () { setHoveredItem(null); }
637 },
638 // Marker
639 el('div', markerProps,
640 renderMarkerContent(item, index),
641 openIconPicker === index && renderIconPicker(index)
642 ),
643
644 // Card
645 el('div', {
646 className: 'bkbg-tl-card' + (a.cardShadow ? ' has-shadow' : '')
647 },
648 // Date
649 el(RichText, {
650 tagName: 'div',
651 className: 'bkbg-tl-date',
652 value: item.date,
653 onChange: function (v) { updateItem(index, 'date', v); },
654 placeholder: __('Date...', 'blockenberg')
655 }),
656
657 // Title
658 el(RichText, {
659 tagName: 'h4',
660 className: 'bkbg-tl-title',
661 value: item.title,
662 onChange: function (v) { updateItem(index, 'title', v); },
663 placeholder: __('Title...', 'blockenberg')
664 }),
665
666 // Content
667 el(RichText, {
668 tagName: 'p',
669 className: 'bkbg-tl-content',
670 value: item.content,
671 onChange: function (v) { updateItem(index, 'content', v); },
672 placeholder: __('Content...', 'blockenberg')
673 })
674 ),
675
676 // Item actions - icons change based on layout
677 el('div', { className: 'bkbg-tl-item-actions' },
678 el(Button, {
679 icon: a.layoutStyle === 'horizontal' ? 'arrow-left-alt2' : 'arrow-up-alt2',
680 label: a.layoutStyle === 'horizontal' ? __('Move Left', 'blockenberg') : __('Move Up', 'blockenberg'),
681 onClick: function () { moveItem(index, -1); },
682 disabled: index === 0,
683 size: 'small'
684 }),
685 el(Button, {
686 icon: a.layoutStyle === 'horizontal' ? 'arrow-right-alt2' : 'arrow-down-alt2',
687 label: a.layoutStyle === 'horizontal' ? __('Move Right', 'blockenberg') : __('Move Down', 'blockenberg'),
688 onClick: function () { moveItem(index, 1); },
689 disabled: index === a.items.length - 1,
690 size: 'small'
691 }),
692 el(Button, {
693 icon: 'plus-alt2',
694 label: __('Insert After', 'blockenberg'),
695 onClick: function () { insertItemAfter(index); },
696 size: 'small'
697 }),
698 el(Button, {
699 icon: 'admin-page',
700 label: __('Duplicate', 'blockenberg'),
701 onClick: function () { duplicateItem(index); },
702 size: 'small'
703 }),
704 el(Button, {
705 icon: 'trash',
706 label: __('Remove', 'blockenberg'),
707 onClick: function () { removeItem(index); },
708 isDestructive: true,
709 disabled: a.items.length <= 1,
710 size: 'small'
711 })
712 )
713 );
714 });
715
716 // Block props
717 var wrapClass = 'bkbg-tl-wrap bkbg-editor-wrap';
718 wrapClass += ' bkbg-tl-' + a.layoutStyle;
719 if (a.alternating && a.layoutStyle === 'vertical') wrapClass += ' bkbg-tl-alternating';
720 if (a.showConnector) wrapClass += ' bkbg-tl-has-connector bkbg-tl-connector-' + a.connectorStyle;
721
722 var blockProps = useBlockProps({
723 className: wrapClass,
724 style: wrapStyle,
725 'data-block-label': 'Timeline'
726 });
727
728 // Toolbar
729 var toolbar = el(BlockControls, {},
730 el(ToolbarGroup, {},
731 el(ToolbarButton, {
732 icon: 'plus-alt2',
733 label: __('Add Item', 'blockenberg'),
734 onClick: addItem
735 })
736 )
737 );
738
739 return el('div', blockProps,
740 inspector,
741 toolbar,
742 el('div', { className: 'bkbg-tl-items' },
743 itemsEl
744 ),
745 el('div', { className: 'bkbg-editor-actions' },
746 el(Button, {
747 variant: 'secondary',
748 onClick: addItem,
749 icon: 'plus-alt2'
750 }, __('Add Event', 'blockenberg'))
751 )
752 );
753 },
754
755 save: function (props) {
756 var a = props.attributes;
757
758 var wrapStyle = {
759 '--bkbg-tl-line-color': a.lineColor,
760 '--bkbg-tl-line-width': a.lineWidth + 'px',
761 '--bkbg-tl-marker-bg': a.markerBg,
762 '--bkbg-tl-marker-color': a.markerColor,
763 '--bkbg-tl-marker-size': a.markerSize + 'px',
764 '--bkbg-tl-card-bg': a.cardBg,
765 '--bkbg-tl-card-border': a.cardBorderColor,
766 '--bkbg-tl-card-border-w': a.cardBorderWidth + 'px',
767 '--bkbg-tl-card-radius': a.cardBorderRadius + 'px',
768 '--bkbg-tl-card-padding': a.cardPadding + 'px',
769 '--bkbg-tl-date-color': a.dateColor,
770 '--bkbg-tl-date-size': a.dateFontSize + 'px',
771 '--bkbg-tl-date-weight': a.dateFontWeight,
772 '--bkbg-tl-title-color': a.titleColor,
773 '--bkbg-tl-title-size': a.titleFontSize + 'px',
774 '--bkbg-tl-title-weight': a.titleFontWeight,
775 '--bkbg-tl-content-color': a.contentColor,
776 '--bkbg-tl-content-size': a.contentFontSize + 'px',
777 '--bkbg-tl-spacing': a.itemSpacing + 'px'
778 };
779
780 // Helper: parse icon value (supports legacy string or new object format)
781 function parseSaveIcon(iconValue) {
782 if (!iconValue) return { type: 'dashicon', value: 'flag' };
783 if (typeof iconValue === 'string') {
784 return { type: 'dashicon', value: iconValue };
785 }
786 return iconValue;
787 }
788
789 var el = wp.element.createElement;
790
791 var wrapClass = 'bkbg-tl-wrap';
792 wrapClass += ' bkbg-tl-' + a.layoutStyle;
793 if (a.alternating && a.layoutStyle === 'vertical') wrapClass += ' bkbg-tl-alternating';
794 if (a.showConnector) wrapClass += ' bkbg-tl-has-connector bkbg-tl-connector-' + a.connectorStyle;
795
796 var itemsEl = a.items.map(function (item, index) {
797 var itemClass = 'bkbg-tl-item';
798 if (a.alternating && a.layoutStyle === 'vertical') {
799 itemClass += index % 2 === 0 ? ' bkbg-tl-item-left' : ' bkbg-tl-item-right';
800 }
801
802 var markerContent;
803 if (a.markerStyle === 'number') {
804 markerContent = el('span', { className: 'bkbg-tl-marker-number' }, index + 1);
805 } else if (a.markerStyle === 'dot') {
806 markerContent = el('span', { className: 'bkbg-tl-marker-dot' });
807 } else {
808 var iconData = parseSaveIcon(item.icon);
809 if (iconData.type === 'custom') {
810 markerContent = el('img', {
811 src: iconData.value,
812 alt: '',
813 className: 'bkbg-tl-marker-img'
814 });
815 } else {
816 // Dashicon
817 markerContent = el('span', {
818 className: 'dashicons dashicons-' + iconData.value + ' bkbg-tl-marker-dashicon'
819 });
820 }
821 }
822
823 return el('div', { key: index, className: itemClass },
824 el('div', { className: 'bkbg-tl-marker' }, markerContent),
825 el('div', { className: 'bkbg-tl-card' + (a.cardShadow ? ' has-shadow' : '') },
826 el('div', { className: 'bkbg-tl-date' }, item.date),
827 el('h4', { className: 'bkbg-tl-title' }, item.title),
828 el('p', { className: 'bkbg-tl-content' }, item.content)
829 )
830 );
831 });
832
833 return el('div', {
834 className: wrapClass,
835 style: wrapStyle,
836 'data-animate': a.animateOnScroll ? '1' : '0',
837 'data-animation': a.animationType
838 },
839 el('div', { className: 'bkbg-tl-items' }, itemsEl)
840 );
841 }
842 });
843 });
844