frontblocks-advanced-option.js
2 months ago
frontblocks-advanced-option.jsx
2 months ago
frontblocks-carousel-editor.css
5 months ago
frontblocks-carousel.css
2 months ago
frontblocks-carousel.js
2 months ago
glide.min.js
10 months ago
frontblocks-advanced-option.jsx
515 lines
| 1 | // Add custom controls to the Advanced panel of GenerateBlocks Grid block |
| 2 | const { addFilter } = wp.hooks; |
| 3 | const { Fragment, useEffect, useRef } = wp.element; |
| 4 | const { InspectorControls, PanelColorSettings, MediaUpload } = wp.blockEditor; |
| 5 | const { SelectControl, TextControl, PanelBody, ToggleControl, Button } = wp.components; |
| 6 | const { __ } = wp.i18n; |
| 7 | |
| 8 | /** |
| 9 | * Returns the document that renders block content. |
| 10 | * WordPress 6.x uses an <iframe> for the editor canvas. |
| 11 | */ |
| 12 | function getEditorDocument() { |
| 13 | const iframe = document.querySelector( 'iframe[name="editor-canvas"]' ); |
| 14 | return ( iframe && iframe.contentDocument && iframe.contentDocument.body ) |
| 15 | ? iframe.contentDocument |
| 16 | : document; |
| 17 | } |
| 18 | |
| 19 | const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-/; |
| 20 | |
| 21 | /** |
| 22 | * Walk an element's subtree to find the container whose DIRECT children |
| 23 | * are actual blocks (UUID data-block). Stops at depth 4. |
| 24 | */ |
| 25 | function findSlidesParent( el, depth ) { |
| 26 | if ( depth === 0 ) return null; |
| 27 | if ( Array.from( el.children ).some( ( c ) => UUID_RE.test( c.dataset.block || '' ) ) ) { |
| 28 | return el; |
| 29 | } |
| 30 | for ( const child of el.children ) { |
| 31 | const found = findSlidesParent( child, depth - 1 ); |
| 32 | if ( found ) return found; |
| 33 | } |
| 34 | return null; |
| 35 | } |
| 36 | |
| 37 | /** |
| 38 | * Find the grid DOM element for a given block. |
| 39 | * |
| 40 | * For generateblocks/element: [data-block][data-type] IS the gb-element. |
| 41 | * Do NOT search inside – that returns a child item. |
| 42 | * For generateblocks/grid: look for .gb-grid-wrapper inside. |
| 43 | * For core/group: walk the subtree to find the element whose children |
| 44 | * are the real blocks, because WP nests them differently across versions. |
| 45 | */ |
| 46 | function findGridEl( blockEl, name ) { |
| 47 | if ( name === 'generateblocks/element' ) { |
| 48 | return blockEl; |
| 49 | } |
| 50 | if ( name === 'generateblocks/grid' ) { |
| 51 | if ( blockEl.classList.contains( 'gb-grid-wrapper' ) ) return blockEl; |
| 52 | const grid = blockEl.querySelector( '.gb-grid-wrapper' ); |
| 53 | return grid || null; |
| 54 | } |
| 55 | if ( name === 'core/group' ) { |
| 56 | // Walk up to 4 levels deep to find where the child blocks live. |
| 57 | return findSlidesParent( blockEl, 4 ) || blockEl; |
| 58 | } |
| 59 | if ( name === 'core/query' ) { |
| 60 | return blockEl.querySelector( '.wp-block-post-template' ) || null; |
| 61 | } |
| 62 | return null; |
| 63 | } |
| 64 | |
| 65 | function addCustomCarouselPanel( BlockEdit ) { |
| 66 | return ( props ) => { |
| 67 | if ( |
| 68 | props.name !== 'generateblocks/grid' && |
| 69 | props.name !== 'generateblocks/element' && |
| 70 | props.name !== 'core/group' && |
| 71 | props.name !== 'core/query' |
| 72 | ) { |
| 73 | return <BlockEdit { ...props } />; |
| 74 | } |
| 75 | |
| 76 | if ( props.name === 'generateblocks/element' ) { |
| 77 | const styles = props.attributes.styles || {}; |
| 78 | if ( styles.display !== 'grid' ) { |
| 79 | return <BlockEdit { ...props } />; |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | if ( props.name === 'core/group' ) { |
| 84 | const layout = props.attributes.layout || {}; |
| 85 | if ( layout.type !== 'grid' ) { |
| 86 | return <BlockEdit { ...props } />; |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | const { |
| 91 | frblGridOption = 'none', |
| 92 | frblItemsToView = '4', |
| 93 | frblLaptopToView = '3', |
| 94 | frblTabletToView = '2', |
| 95 | frblResponsiveToView = '1', |
| 96 | frblAutoplay = '', |
| 97 | frblGap = '20', |
| 98 | frblButtons = 'arrows', |
| 99 | frblRewind = true, |
| 100 | frblButtonColor, |
| 101 | frblButtonBgColor, |
| 102 | frblButtonsPosition = 'side', |
| 103 | frblArrowLeftUrl = '', |
| 104 | frblArrowRightUrl = '', |
| 105 | frblDisableOnDesktop = false, |
| 106 | } = props.attributes; |
| 107 | |
| 108 | // ── Editor carousel preview ────────────────────────────────────────── |
| 109 | const stateRef = useRef( null ); |
| 110 | |
| 111 | useEffect( () => { |
| 112 | let mounted = true; |
| 113 | let timer = null; |
| 114 | |
| 115 | function cleanup() { |
| 116 | if ( ! stateRef.current ) return; |
| 117 | const { gridEl, slides, spacer, prevBtn, nextBtn, resizeObs, scrollFn } = stateRef.current; |
| 118 | |
| 119 | // Disconnect observer and scroll listener. |
| 120 | if ( resizeObs ) resizeObs.disconnect(); |
| 121 | const editorDoc = getEditorDocument(); |
| 122 | const editorScrollEl = editorDoc.documentElement || editorDoc.body; |
| 123 | if ( scrollFn ) editorScrollEl.removeEventListener( 'scroll', scrollFn, true ); |
| 124 | |
| 125 | // Remove arrows from outer window body (completely outside React). |
| 126 | [ prevBtn, nextBtn ].forEach( ( btn ) => { |
| 127 | if ( btn && btn.parentNode ) btn.parentNode.removeChild( btn ); |
| 128 | } ); |
| 129 | |
| 130 | // Remove right spacer element. |
| 131 | if ( spacer && spacer.parentNode ) spacer.parentNode.removeChild( spacer ); |
| 132 | |
| 133 | // Restore gridEl styles. |
| 134 | try { |
| 135 | const { preventScroll } = stateRef.current; |
| 136 | if ( preventScroll ) { |
| 137 | gridEl.removeEventListener( 'wheel', preventScroll ); |
| 138 | gridEl.removeEventListener( 'touchstart', preventScroll ); |
| 139 | gridEl.removeEventListener( 'touchmove', preventScroll ); |
| 140 | } |
| 141 | gridEl.classList.remove( 'frbl-carousel-noscrollbar' ); |
| 142 | [ |
| 143 | 'display', 'flex-wrap', 'gap', |
| 144 | 'overflow-x', 'scroll-behavior', |
| 145 | 'padding-left', 'padding-right', |
| 146 | 'max-width', 'grid-template-columns', |
| 147 | ].forEach( ( p ) => gridEl.style.removeProperty( p ) ); |
| 148 | |
| 149 | slides.forEach( ( slide ) => { |
| 150 | [ 'flex-shrink', 'width', 'min-width', |
| 151 | 'margin-left', 'margin-right', 'grid-column', 'grid-row', |
| 152 | ].forEach( ( p ) => slide.style.removeProperty( p ) ); |
| 153 | } ); |
| 154 | } catch ( e ) {} |
| 155 | |
| 156 | stateRef.current = null; |
| 157 | } |
| 158 | |
| 159 | function init() { |
| 160 | if ( ! mounted ) return; |
| 161 | cleanup(); |
| 162 | |
| 163 | const editorDoc = getEditorDocument(); |
| 164 | |
| 165 | // GB 2.x wraps blocks in a root element that shares the same data-block UUID. |
| 166 | // Use data-type to select the correct inner element; fall back for native blocks. |
| 167 | let blockEl = editorDoc.querySelector( |
| 168 | `[data-block="${ props.clientId }"][data-type="${ props.name }"]` |
| 169 | ); |
| 170 | if ( ! blockEl ) { |
| 171 | blockEl = editorDoc.querySelector( `[data-block="${ props.clientId }"]` ); |
| 172 | } |
| 173 | if ( ! blockEl ) return; |
| 174 | |
| 175 | const gridEl = findGridEl( blockEl, props.name ); |
| 176 | if ( ! gridEl ) return; |
| 177 | |
| 178 | // For core/query, <li> items don't have data-block UUIDs (server-rendered). |
| 179 | // For other blocks, filter by UUID data-block attribute. |
| 180 | const isQueryLoop = props.name === 'core/query'; |
| 181 | const slides = isQueryLoop |
| 182 | ? Array.from( gridEl.children ).filter( ( el ) => el.tagName === 'LI' ) |
| 183 | : Array.from( gridEl.children ).filter( ( el ) => UUID_RE.test( el.dataset.block || '' ) ); |
| 184 | if ( slides.length === 0 ) return; |
| 185 | |
| 186 | const perView = Math.max( 1, parseInt( frblItemsToView ) || 4 ); |
| 187 | const gap = Math.max( 0, parseInt( frblGap ) || 20 ); |
| 188 | const btnColor = frblButtonColor || '#fff'; |
| 189 | const btnBg = frblButtonBgColor || 'rgba(0,0,0,0.45)'; |
| 190 | |
| 191 | // For core/query, force flex via setProperty to beat WP's grid CSS. |
| 192 | if ( isQueryLoop ) { |
| 193 | gridEl.style.setProperty( 'display', 'flex', 'important' ); |
| 194 | gridEl.style.setProperty( 'flex-wrap', 'nowrap', 'important' ); |
| 195 | gridEl.style.setProperty( 'max-width', 'none', 'important' ); |
| 196 | gridEl.style.setProperty( 'grid-template-columns', 'none', 'important' ); |
| 197 | gridEl.style.setProperty( 'list-style', 'none', 'important' ); |
| 198 | gridEl.style.setProperty( 'padding-left', '0', 'important' ); |
| 199 | } |
| 200 | |
| 201 | // Measure content-box width before any style changes. |
| 202 | const editorWin = editorDoc.defaultView || window; |
| 203 | const ARROW_W = 40; // px reserved on each side for arrow buttons. |
| 204 | const cs = editorWin.getComputedStyle( gridEl ); |
| 205 | const innerW = gridEl.getBoundingClientRect().width |
| 206 | - ( parseFloat( cs.paddingLeft ) || 0 ) |
| 207 | - ( parseFloat( cs.paddingRight ) || 0 ); |
| 208 | const totalWidth = Math.round( innerW ) || 600; |
| 209 | // Content area after reserving space for both arrow buttons. |
| 210 | const contentW = totalWidth - ARROW_W * 2; |
| 211 | const slideWidth = Math.round( ( contentW - gap * ( perView - 1 ) ) / perView ); |
| 212 | const step = slideWidth + gap; |
| 213 | |
| 214 | // ── Apply scroll-based layout directly to gridEl ───────────── |
| 215 | // No DOM restructuring → no React reconciliation conflicts. |
| 216 | gridEl.classList.add( 'frbl-carousel-noscrollbar' ); |
| 217 | if ( ! isQueryLoop ) { |
| 218 | gridEl.style.display = 'flex'; |
| 219 | gridEl.style.flexWrap = 'nowrap'; |
| 220 | gridEl.style.paddingLeft = ARROW_W + 'px'; |
| 221 | } else { |
| 222 | // paddingLeft already set via setProperty above; override with arrow offset. |
| 223 | gridEl.style.setProperty( 'padding-left', ARROW_W + 'px', 'important' ); |
| 224 | } |
| 225 | gridEl.style.gap = gap + 'px'; |
| 226 | gridEl.style.overflowX = 'scroll'; |
| 227 | gridEl.style.scrollBehavior = 'smooth'; |
| 228 | gridEl.style.paddingRight = '0'; |
| 229 | |
| 230 | slides.forEach( ( slide ) => { |
| 231 | slide.style.flexShrink = '0'; |
| 232 | slide.style.width = slideWidth + 'px'; |
| 233 | slide.style.minWidth = slideWidth + 'px'; |
| 234 | slide.style.marginLeft = '0'; |
| 235 | slide.style.marginRight = '0'; |
| 236 | slide.style.gridColumn = 'unset'; |
| 237 | slide.style.gridRow = 'unset'; |
| 238 | } ); |
| 239 | |
| 240 | // Right-side spacer: Chrome does not include padding-right in horizontal |
| 241 | // scroll extent, so we use a real flex child to guarantee right space. |
| 242 | const appender = gridEl.querySelector( '.block-list-appender' ); |
| 243 | const spacer = editorDoc.createElement( 'span' ); |
| 244 | spacer.setAttribute( 'data-frbl-spacer', '1' ); |
| 245 | spacer.style.cssText = `display:block;flex-shrink:0;width:${ ARROW_W }px;min-width:${ ARROW_W }px;`; |
| 246 | gridEl.insertBefore( spacer, appender || null ); |
| 247 | |
| 248 | // Prevent manual scroll — only arrows drive navigation. |
| 249 | const preventScroll = ( e ) => e.preventDefault(); |
| 250 | gridEl.addEventListener( 'wheel', preventScroll, { passive: false } ); |
| 251 | gridEl.addEventListener( 'touchstart', preventScroll, { passive: false } ); |
| 252 | gridEl.addEventListener( 'touchmove', preventScroll, { passive: false } ); |
| 253 | |
| 254 | // ── Navigation (scrollLeft, item-by-item) ──────────────────── |
| 255 | let idx = 0; |
| 256 | const lastIdx = Math.max( 0, slides.length - perView ); |
| 257 | |
| 258 | function scrollTo( n ) { |
| 259 | if ( n > lastIdx ) { |
| 260 | // Wrap forward to start. |
| 261 | gridEl.style.scrollBehavior = 'auto'; |
| 262 | gridEl.scrollLeft = 0; |
| 263 | void gridEl.offsetWidth; |
| 264 | gridEl.style.scrollBehavior = 'smooth'; |
| 265 | idx = 0; |
| 266 | return; |
| 267 | } |
| 268 | if ( n < 0 ) { |
| 269 | // Wrap back to end. |
| 270 | gridEl.style.scrollBehavior = 'auto'; |
| 271 | gridEl.scrollLeft = Math.round( lastIdx * step ); |
| 272 | void gridEl.offsetWidth; |
| 273 | gridEl.style.scrollBehavior = 'smooth'; |
| 274 | idx = lastIdx; |
| 275 | return; |
| 276 | } |
| 277 | idx = n; |
| 278 | gridEl.scrollLeft = Math.round( idx * step ); |
| 279 | } |
| 280 | |
| 281 | // ── Arrows in OUTER document body ──────────────────────────── |
| 282 | // Placing them outside the iframe means React never touches them. |
| 283 | const iframe = document.querySelector( 'iframe[name="editor-canvas"]' ); |
| 284 | |
| 285 | function updateArrowPos() { |
| 286 | if ( ! stateRef.current ) return; |
| 287 | const ifrRect = iframe ? iframe.getBoundingClientRect() : { top: 0, left: 0 }; |
| 288 | const rect = gridEl.getBoundingClientRect(); // coords in iframe viewport |
| 289 | const midY = Math.round( ifrRect.top + rect.top + rect.height / 2 - 16 ); |
| 290 | |
| 291 | prevBtn.style.top = midY + 'px'; |
| 292 | prevBtn.style.left = Math.round( ifrRect.left + rect.left + 8 ) + 'px'; |
| 293 | nextBtn.style.top = midY + 'px'; |
| 294 | nextBtn.style.right = Math.round( window.innerWidth - ( ifrRect.left + rect.right ) + 8 ) + 'px'; |
| 295 | } |
| 296 | |
| 297 | function makeArrow( dir ) { |
| 298 | const btn = document.createElement( 'button' ); |
| 299 | btn.type = 'button'; |
| 300 | btn.className = `frbl-editor-arrow frbl-editor-arrow-${ dir }`; |
| 301 | btn.setAttribute( 'aria-label', dir === 'prev' ? 'Previous slide' : 'Next slide' ); |
| 302 | btn.style.cssText = `position:fixed;z-index:99999;background-color:${ btnBg };`; |
| 303 | const d = dir === 'prev' ? 'M6 1L1 6L6 11' : 'M1 11L6 6L1 1'; |
| 304 | btn.innerHTML = `<svg width="7" height="12" viewBox="0 0 7 12" fill="none"><path d="${ d }" stroke="${ btnColor }" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`; |
| 305 | btn.addEventListener( 'click', ( e ) => { |
| 306 | e.stopPropagation(); |
| 307 | scrollTo( dir === 'prev' ? idx - 1 : idx + 1 ); |
| 308 | } ); |
| 309 | document.body.appendChild( btn ); |
| 310 | return btn; |
| 311 | } |
| 312 | |
| 313 | const prevBtn = makeArrow( 'prev' ); |
| 314 | const nextBtn = makeArrow( 'next' ); |
| 315 | updateArrowPos(); |
| 316 | |
| 317 | // Keep arrows positioned correctly when the editor scrolls or resizes. |
| 318 | const resizeObs = new ResizeObserver( updateArrowPos ); |
| 319 | resizeObs.observe( gridEl ); |
| 320 | |
| 321 | const editorScrollEl = editorDoc.documentElement || editorDoc.body; |
| 322 | const scrollFn = updateArrowPos; |
| 323 | editorScrollEl.addEventListener( 'scroll', scrollFn, { passive: true, capture: true } ); |
| 324 | |
| 325 | stateRef.current = { gridEl, slides, spacer, prevBtn, nextBtn, resizeObs, scrollFn, preventScroll }; |
| 326 | } |
| 327 | |
| 328 | if ( frblGridOption !== 'none' ) { |
| 329 | timer = setTimeout( init, 300 ); |
| 330 | } else { |
| 331 | cleanup(); |
| 332 | } |
| 333 | |
| 334 | return () => { |
| 335 | mounted = false; |
| 336 | if ( timer ) clearTimeout( timer ); |
| 337 | cleanup(); |
| 338 | }; |
| 339 | }, [ |
| 340 | frblGridOption, frblItemsToView, frblGap, |
| 341 | frblButtonColor, frblButtonBgColor, props.clientId, |
| 342 | ] ); |
| 343 | // ── Inspector panel ────────────────────────────────────────────────── |
| 344 | |
| 345 | return ( |
| 346 | <Fragment> |
| 347 | <BlockEdit { ...props } /> |
| 348 | <InspectorControls> |
| 349 | <PanelBody |
| 350 | title={ __( 'FrontBlocks - Carousel', 'frontblocks' ) } |
| 351 | initialOpen={ true } |
| 352 | > |
| 353 | <SelectControl |
| 354 | label={ __( 'FrontBlocks Grid Option', 'frontblocks' ) } |
| 355 | value={ frblGridOption } |
| 356 | options={ [ |
| 357 | { label: __( 'None', 'frontblocks' ), value: 'none' }, |
| 358 | { label: __( 'Carousel', 'frontblocks' ), value: 'carousel' }, |
| 359 | { label: __( 'Slider', 'frontblocks' ), value: 'slider' }, |
| 360 | ] } |
| 361 | onChange={ ( value ) => props.setAttributes( { frblGridOption: value } ) } |
| 362 | help={ __( 'This option gives the option to make carousel in your grid block.', 'frontblocks' ) } |
| 363 | /> |
| 364 | { frblGridOption !== 'none' && ( |
| 365 | <> |
| 366 | <TextControl |
| 367 | label={ __( 'Items to view (Desktop)', 'frontblocks' ) } |
| 368 | value={ frblItemsToView } |
| 369 | onChange={ ( value ) => props.setAttributes( { frblItemsToView: value } ) } |
| 370 | help={ __( 'Number of items to show on desktop (>1200px)', 'frontblocks' ) } |
| 371 | /> |
| 372 | <TextControl |
| 373 | label={ __( 'Items to view (Laptop)', 'frontblocks' ) } |
| 374 | value={ frblLaptopToView } |
| 375 | onChange={ ( value ) => props.setAttributes( { frblLaptopToView: value } ) } |
| 376 | help={ __( 'Number of items to show on laptop (992px-1199px)', 'frontblocks' ) } |
| 377 | /> |
| 378 | <TextControl |
| 379 | label={ __( 'Items to view (Tablet)', 'frontblocks' ) } |
| 380 | value={ frblTabletToView } |
| 381 | onChange={ ( value ) => props.setAttributes( { frblTabletToView: value } ) } |
| 382 | help={ __( 'Number of items to show on tablet (768px-991px)', 'frontblocks' ) } |
| 383 | /> |
| 384 | <TextControl |
| 385 | label={ __( 'Items to view (Mobile)', 'frontblocks' ) } |
| 386 | value={ frblResponsiveToView } |
| 387 | onChange={ ( value ) => props.setAttributes( { frblResponsiveToView: value } ) } |
| 388 | help={ __( 'Number of items to show on mobile (<768px)', 'frontblocks' ) } |
| 389 | /> |
| 390 | <TextControl |
| 391 | label={ __( 'Autoplay (seconds)', 'frontblocks' ) } |
| 392 | value={ frblAutoplay } |
| 393 | onChange={ ( value ) => props.setAttributes( { frblAutoplay: value } ) } |
| 394 | /> |
| 395 | <TextControl |
| 396 | label={ __( 'Gap (px)', 'frontblocks' ) } |
| 397 | value={ frblGap } |
| 398 | onChange={ ( value ) => props.setAttributes( { frblGap: value } ) } |
| 399 | help={ __( 'Space between slides in pixels. Leave empty for 20.', 'frontblocks' ) } |
| 400 | /> |
| 401 | { frblGridOption === 'slider' && ( |
| 402 | <ToggleControl |
| 403 | label={ __( 'Rewind', 'frontblocks' ) } |
| 404 | checked={ frblRewind } |
| 405 | onChange={ ( value ) => props.setAttributes( { frblRewind: value } ) } |
| 406 | /> |
| 407 | ) } |
| 408 | <SelectControl |
| 409 | label={ __( 'Buttons', 'frontblocks' ) } |
| 410 | value={ frblButtons } |
| 411 | options={ [ |
| 412 | { label: __( 'None', 'frontblocks' ), value: 'none' }, |
| 413 | { label: __( 'Bullets', 'frontblocks' ), value: 'bullets' }, |
| 414 | { label: __( 'Arrows', 'frontblocks' ), value: 'arrows' }, |
| 415 | ] } |
| 416 | onChange={ ( value ) => props.setAttributes( { frblButtons: value } ) } |
| 417 | /> |
| 418 | { frblButtons === 'arrows' && ( |
| 419 | <> |
| 420 | <SelectControl |
| 421 | label={ __( 'Buttons Position', 'frontblocks' ) } |
| 422 | value={ frblButtonsPosition } |
| 423 | options={ [ |
| 424 | { label: __( 'Side (left & right)', 'frontblocks' ), value: 'side' }, |
| 425 | { label: __( 'Bottom left', 'frontblocks' ), value: 'bottom-left' }, |
| 426 | { label: __( 'Bottom right', 'frontblocks' ), value: 'bottom-right' }, |
| 427 | { label: __( 'Top left', 'frontblocks' ), value: 'top-left' }, |
| 428 | { label: __( 'Top right', 'frontblocks' ), value: 'top-right' }, |
| 429 | ] } |
| 430 | onChange={ ( value ) => props.setAttributes( { frblButtonsPosition: value } ) } |
| 431 | /> |
| 432 | <div style={ { marginBottom: '12px' } }> |
| 433 | <p style={ { marginBottom: '4px', fontWeight: 500 } }>{ __( 'Left arrow icon', 'frontblocks' ) }</p> |
| 434 | { frblArrowLeftUrl && ( |
| 435 | <img src={ frblArrowLeftUrl } alt="" style={ { display: 'block', width: '32px', height: '32px', marginBottom: '6px', objectFit: 'contain' } } /> |
| 436 | ) } |
| 437 | <MediaUpload |
| 438 | onSelect={ ( media ) => props.setAttributes( { frblArrowLeftUrl: media.url } ) } |
| 439 | allowedTypes={ [ 'image' ] } |
| 440 | value={ frblArrowLeftUrl } |
| 441 | render={ ( { open } ) => ( |
| 442 | <div style={ { display: 'flex', gap: '6px', flexWrap: 'wrap' } }> |
| 443 | <Button onClick={ open } variant="secondary" size="small"> |
| 444 | { frblArrowLeftUrl ? __( 'Change', 'frontblocks' ) : __( 'Select SVG', 'frontblocks' ) } |
| 445 | </Button> |
| 446 | { frblArrowLeftUrl && ( |
| 447 | <Button onClick={ () => props.setAttributes( { frblArrowLeftUrl: '' } ) } variant="link" isDestructive size="small"> |
| 448 | { __( 'Remove', 'frontblocks' ) } |
| 449 | </Button> |
| 450 | ) } |
| 451 | </div> |
| 452 | ) } |
| 453 | /> |
| 454 | </div> |
| 455 | <div style={ { marginBottom: '12px' } }> |
| 456 | <p style={ { marginBottom: '4px', fontWeight: 500 } }>{ __( 'Right arrow icon', 'frontblocks' ) }</p> |
| 457 | { frblArrowRightUrl && ( |
| 458 | <img src={ frblArrowRightUrl } alt="" style={ { display: 'block', width: '32px', height: '32px', marginBottom: '6px', objectFit: 'contain' } } /> |
| 459 | ) } |
| 460 | <MediaUpload |
| 461 | onSelect={ ( media ) => props.setAttributes( { frblArrowRightUrl: media.url } ) } |
| 462 | allowedTypes={ [ 'image' ] } |
| 463 | value={ frblArrowRightUrl } |
| 464 | render={ ( { open } ) => ( |
| 465 | <div style={ { display: 'flex', gap: '6px', flexWrap: 'wrap' } }> |
| 466 | <Button onClick={ open } variant="secondary" size="small"> |
| 467 | { frblArrowRightUrl ? __( 'Change', 'frontblocks' ) : __( 'Select SVG', 'frontblocks' ) } |
| 468 | </Button> |
| 469 | { frblArrowRightUrl && ( |
| 470 | <Button onClick={ () => props.setAttributes( { frblArrowRightUrl: '' } ) } variant="link" isDestructive size="small"> |
| 471 | { __( 'Remove', 'frontblocks' ) } |
| 472 | </Button> |
| 473 | ) } |
| 474 | </div> |
| 475 | ) } |
| 476 | /> |
| 477 | </div> |
| 478 | </> |
| 479 | ) } |
| 480 | <PanelColorSettings |
| 481 | title={ __( 'Button Colors', 'frontblocks' ) } |
| 482 | colorSettings={ [ |
| 483 | { |
| 484 | value: frblButtonColor, |
| 485 | onChange: ( color ) => props.setAttributes( { frblButtonColor: color } ), |
| 486 | label: __( 'Color button', 'frontblocks' ), |
| 487 | }, |
| 488 | { |
| 489 | value: frblButtonBgColor, |
| 490 | onChange: ( color ) => props.setAttributes( { frblButtonBgColor: color } ), |
| 491 | label: __( 'Color background button', 'frontblocks' ), |
| 492 | }, |
| 493 | ] } |
| 494 | /> |
| 495 | <ToggleControl |
| 496 | label={ __( 'Disable on Desktop', 'frontblocks' ) } |
| 497 | checked={ frblDisableOnDesktop } |
| 498 | onChange={ ( value ) => props.setAttributes( { frblDisableOnDesktop: value } ) } |
| 499 | help={ __( 'If enabled, carousel/slider will only work on mobile devices.', 'frontblocks' ) } |
| 500 | /> |
| 501 | </> |
| 502 | ) } |
| 503 | </PanelBody> |
| 504 | </InspectorControls> |
| 505 | </Fragment> |
| 506 | ); |
| 507 | }; |
| 508 | } |
| 509 | |
| 510 | addFilter( |
| 511 | 'editor.BlockEdit', |
| 512 | 'frontblocks/gb-grid-carousel-panel', |
| 513 | addCustomCarouselPanel |
| 514 | ); |
| 515 |