diff --git a/assets/infotip/infotip-web-component.js b/assets/infotip/infotip-web-component.js index 9c9edba..6d43c35 100644 --- a/assets/infotip/infotip-web-component.js +++ b/assets/infotip/infotip-web-component.js @@ -18,46 +18,72 @@ class BlaBlaBlocksInfotip extends HTMLElement { } connectedCallback() { - const template = this.renderElement(); this.attachShadow( { mode: 'open' } ); - this.shadowRoot.appendChild( template.content.cloneNode( true ) ); - - // Add this section to handle initial icon state - const iconEnabled = this.getAttribute( 'icon-enabled' ) === 'true'; - if ( iconEnabled ) { - const icon = this.shadowRoot.querySelector( '.icon' ); - icon.innerHTML = this.renderIcon( - this.getAttribute( 'icon-type' ) || 'info' - ); - } + this.shadowRoot.appendChild( + this.renderElement().content.cloneNode( true ) + ); + this.updateIcon(); requestAnimationFrame( () => { - // Wait for the element to be attached to the DOM. this.updatePosition(); this.initializeEventListeners(); + this.hideTooltip(); } ); } + // Renders the main template for the component + renderElement() { + const content = this.getAttribute( 'content' ); + const template = document.createElement( 'template' ); + template.innerHTML = ` + + + + + + +
+
+ ${ content } +
+
+
+
+ `; + return template; + } + + // Updates the icon based on current attributes + updateIcon() { + const iconEnabled = this.getAttribute( 'icon-enabled' ) === 'true'; + const icon = this.shadowRoot.querySelector( '.icon' ); + if ( ! icon ) return; + icon.innerHTML = iconEnabled + ? this.renderIcon( this.getAttribute( 'icon-type' ) || 'info' ) + : ''; + } + + // Updates the infotip overlay position using FloatingUIDOM updatePosition() { - // Prevent the infotip overlay from showing when content is blank. if ( this.getAttribute( 'content' ) === '' ) { this.hideTooltip(); return; } - const floatingUIDOM = window.FloatingUIDOM; const anchorText = this.shadowRoot.querySelector( '.text' ); const infotip = this.shadowRoot.querySelector( '.infotip' ); const arrow = infotip.querySelector( '.arrow' ); const overlayPlacement = this.getAttribute( 'overlay-placement' ) ?? 'top'; - const offset = this.getAttribute( 'offset' ) ?? 6; + const offset = parseInt( this.getAttribute( 'offset' ) ?? 6, 10 ); floatingUIDOM .computePosition( anchorText, infotip, { placement: overlayPlacement, strategy: 'fixed', middleware: [ - floatingUIDOM.offset( parseInt( offset, 10 ) ), + floatingUIDOM.offset( offset ), floatingUIDOM.flip( { fallbackPlacements: [ 'top', @@ -75,38 +101,38 @@ class BlaBlaBlocksInfotip extends HTMLElement { left: `${ x }px`, top: `${ y }px`, } ); - - // Extract the base placement (first part before any hyphen) - const basePlacement = placement.split( '-' )[ 0 ]; - - // Arrow positioning - if ( middlewareData.arrow ) { - const { x: arrowX, y: arrowY } = middlewareData.arrow; - - // Reset all positions first - arrow.style.left = ''; - arrow.style.top = ''; - arrow.style.right = ''; - arrow.style.bottom = ''; - - // Set the arrow position based on the base placement - if ( basePlacement === 'top' ) { - arrow.style.bottom = '-4px'; - arrow.style.left = arrowX ? `${ arrowX }px` : ''; - } else if ( basePlacement === 'bottom' ) { - arrow.style.top = '-4px'; - arrow.style.left = arrowX ? `${ arrowX }px` : ''; - } else if ( basePlacement === 'left' ) { - arrow.style.right = '-4px'; - arrow.style.top = arrowY ? `${ arrowY }px` : ''; - } else if ( basePlacement === 'right' ) { - arrow.style.left = '-4px'; - arrow.style.top = arrowY ? `${ arrowY }px` : ''; - } - } + this.positionArrow( arrow, placement, middlewareData.arrow ); } ); } + // Positions the arrow based on placement and middleware data + positionArrow( arrow, placement, arrowData ) { + if ( ! arrow || ! arrowData ) return; + const basePlacement = placement.split( '-' )[ 0 ]; + arrow.style.left = ''; + arrow.style.top = ''; + arrow.style.right = ''; + arrow.style.bottom = ''; + switch ( basePlacement ) { + case 'top': + arrow.style.bottom = '-4px'; + arrow.style.left = arrowData.x ? `${ arrowData.x }px` : ''; + break; + case 'bottom': + arrow.style.top = '-4px'; + arrow.style.left = arrowData.x ? `${ arrowData.x }px` : ''; + break; + case 'left': + arrow.style.right = '-4px'; + arrow.style.top = arrowData.y ? `${ arrowData.y }px` : ''; + break; + case 'right': + arrow.style.left = '-4px'; + arrow.style.top = arrowData.y ? `${ arrowData.y }px` : ''; + break; + } + } + showTooltip() { this.shadowRoot.querySelector( '.infotip' ).style.display = 'block'; this.updatePosition(); @@ -116,17 +142,20 @@ class BlaBlaBlocksInfotip extends HTMLElement { this.shadowRoot.querySelector( '.infotip' ).style.display = 'none'; } + // Sets up mouse and keyboard event listeners for showing/hiding the tooltip initializeEventListeners() { - [ + const events = [ [ 'mouseenter', this.showTooltip.bind( this ) ], [ 'mouseleave', this.hideTooltip.bind( this ) ], [ 'focus', this.showTooltip.bind( this ) ], [ 'blur', this.hideTooltip.bind( this ) ], - ].forEach( ( [ event, listener ] ) => { + ]; + events.forEach( ( [ event, listener ] ) => { this.addEventListener( event, listener ); } ); } + // Generates the component's CSS based on current attributes renderStyle() { const showUnderline = this.getAttribute( 'underline' ) !== 'false'; const iconEnabled = this.getAttribute( 'icon-enabled' ) === 'true'; @@ -183,125 +212,58 @@ class BlaBlaBlocksInfotip extends HTMLElement { } `; } - return style; } + // Returns SVG markup for the given icon type renderIcon( iconType = 'info' ) { const iconPaths = { - info: ``, + info: ``, - help: ``, + help: ``, - caution: ``, + caution: ``, - error: ``, + error: ``, - notAllowed: ``, + notAllowed: ``, - starEmpty: ``, + starEmpty: ``, }; return ``; - } - - renderElement() { - const content = this.getAttribute( 'content' ); - const template = document.createElement( 'template' ); - template.innerHTML = ` - - - - - - -
-
- ${ content } -
-
-
-
- `; - - return template; + ${ iconPaths[ iconType ] || iconPaths.info } + `; } + // Handles attribute changes and updates the component accordingly attributeChangedCallback( name, oldValue, newValue ) { const shadow = this.shadowRoot; - if ( ! shadow ) { - return; + if ( ! shadow ) return; + + switch ( name ) { + case 'content': + shadow.querySelector( '.infotip-popover-content' ).innerHTML = + newValue; + this.updatePosition(); + this.showTooltip(); + break; + case 'icon-enabled': + case 'icon-type': + this.updateIcon(); + this.updatePosition(); + break; + case 'overlay-text-color': + case 'overlay-background-color': + this.showTooltip(); + break; + case 'offset': + case 'overlay-placement': + this.updatePosition(); + this.showTooltip(); + break; } - - if ( name === 'content' ) { - const infotip = shadow.querySelector( '.infotip-popover-content' ); - infotip.innerHTML = newValue; - - this.updatePosition(); - this.showTooltip(); - } - - if ( name === 'icon-enabled' ) { - const icon = shadow.querySelector( '.icon' ); - - if ( newValue === 'true' ) { - icon.innerHTML = this.renderIcon( - this.getAttribute( 'icon-type' ) || 'info' - ); - } else { - icon.innerHTML = ''; - } - - this.updatePosition(); - } - - if ( name === 'icon-type' ) { - const icon = shadow.querySelector( '.icon' ); - if ( this.getAttribute( 'icon-enabled' ) === 'true' ) { - icon.innerHTML = this.renderIcon( newValue ); - } - this.updatePosition(); - } - - if ( - name === 'overlay-text-color' || - name === 'overlay-background-color' - ) { - this.showTooltip(); - } - - if ( name === 'offset' || name === 'overlay-placement' ) { - this.updatePosition(); - this.showTooltip(); - } - - const style = shadow.querySelector( 'style' ); - style.textContent = this.renderStyle(); + // Always update styles on attribute change + shadow.querySelector( 'style' ).textContent = this.renderStyle(); } } diff --git a/src/infotip/index.js b/src/infotip/index.js index 4989400..5386dc1 100644 --- a/src/infotip/index.js +++ b/src/infotip/index.js @@ -3,7 +3,7 @@ */ import { info } from '@wordpress/icons'; import { RichTextToolbarButton } from '@wordpress/block-editor'; -import { useState } from '@wordpress/element'; +import { useState, useEffect } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; /** @@ -28,6 +28,12 @@ function EditButton( props ) { const [ isSettingOpen, setIsSettingOpen ] = useState( false ); + useEffect( () => { + if ( ! isActive ) { + setIsSettingOpen( false ); + } + }, [ isActive ] ); + return ( <> { + const sanitizedValue = safeHTML( newValue ); + updateAttributes( { content: sanitizedValue } ); + }; + + const handleUnderlineToggle = () => { + if ( isUnderlined ) { + updateAttributes( { underline: 'false' } ); + } else { + removeAttributes( [ 'underline' ] ); + } + }; + + const handleClear = () => { + onChange( removeFormat( value, name ) ); + if ( onClose ) onClose(); + }; + return ( { - const sanitizedValue = safeHTML( newValue ); - - updateAttributes( { - content: sanitizedValue, - } ); - } } + onChange={ handleTextChange } placeholder={ __( 'Enter the text to display, or click clear to remove the format.', 'blablablocks-formats' @@ -78,23 +136,14 @@ function TextTabContent( { id="underline-toggle" label={ __( 'Underline anchor text', 'blablablocks-formats' ) } checked={ isUnderlined } - onChange={ () => { - if ( isUnderlined ) { - updateAttributes( { underline: 'false' } ); - } else { - removeAttributes( [ 'underline' ] ); - } - } } + onChange={ handleUnderlineToggle } __nextHasNoMarginBottom={ true } />