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

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

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