diff --git a/inc/classes/admin/script/loader.class.php b/inc/classes/admin/script/loader.class.php index c0a6a70a..d7cf3376 100644 --- a/inc/classes/admin/script/loader.class.php +++ b/inc/classes/admin/script/loader.class.php @@ -231,18 +231,9 @@ public static function get_common_scripts() { 'base' => \THE_SEO_FRAMEWORK_DIR_URL . 'lib/css/', 'ver' => \THE_SEO_FRAMEWORK_VERSION, 'inline' => [ - '.tsf-tooltip-text-wrap' => [ - 'background-color:{{$bg_accent}}', - 'color:{{$rel_bg_accent}}', - ], - '.tsf-tooltip-text-wrap *' => [ - 'color:{{$rel_bg_accent}}', - ], - '.tsf-tooltip-arrow:after' => [ - 'border-top-color:{{$bg_accent}}', - ], - '.tsf-tooltip-down .tsf-tooltip-arrow:after' => [ - 'border-bottom-color:{{$bg_accent}}', + ':root' => [ + '--tsf-user-colors-bg-accent:{{$bg_accent}}', + '--tsf-user-colors-fg-accent:{{$rel_bg_accent}}', ], '.tsf-tooltip-text' => [ \is_rtl() ? 'direction:rtl' : '', @@ -539,12 +530,21 @@ public static function get_gutenberg_compat_scripts() { [ 'id' => 'tsf-gbc', 'type' => 'js', - 'deps' => [ 'jquery', 'tsf', 'tsf-utils', 'wp-editor', 'wp-data', 'react' ], + 'deps' => [ 'jquery', 'tsf', 'tsf-utils', 'wp-editor', 'wp-data', 'react', 'wp-element', 'wp-components' ], 'autoload' => true, 'name' => 'gbc', 'base' => \THE_SEO_FRAMEWORK_DIR_URL . 'lib/js/', 'ver' => \THE_SEO_FRAMEWORK_VERSION, ], + [ + 'id' => 'tsf-gbc', + 'type' => 'css', + 'deps' => [ 'wp-components' ], + 'autoload' => true, + 'name' => 'gbc', + 'base' => \THE_SEO_FRAMEWORK_DIR_URL . 'lib/css/', + 'ver' => \THE_SEO_FRAMEWORK_VERSION, + ], ]; } diff --git a/lib/css/gbc.css b/lib/css/gbc.css new file mode 100644 index 00000000..cb9e1b34 --- /dev/null +++ b/lib/css/gbc.css @@ -0,0 +1,56 @@ +:root { + /* Prevents a shift from the scrollbar gutter being added that happens if the tooltip popover + overflows the viewport. The popover repositions to not overflow and the shift isn’t painted + yet the wrap element’s rectangle gets read at the shifted position and the tooltip arrow + displayed there only to jump in the next animation frame. This seems safe to add although + ideally Gutenberg might add this itself. */ + overflow-x: hidden; +} + +.tsf-gbc-tooltip-popover { + pointer-events: none; + filter: drop-shadow(0 0 1px rgba( 0, 0, 0, .6 ) ); +} + +.tsf-gbc-tooltip-popover > .components-popover__content { + /* Unset the Gutenberg popover’s min-content width that would otherwise require + setting width or min-width on either the tooltip or its content. */ + width: unset; +} + +.tsf-tooltip-arrow { + /* Hide our arrow in Gutenberg (to use theirs instead). */ + display: none; +} + +.tsf-gbc-tooltip-popover > .components-popover__arrow { + width: 16px; + height: 16px; +} + +.tsf-gbc-tooltip-popover > .components-popover__arrow:before { + /* Hide Gutenberg’s arrow’s before pseudo because it’s only used to whiteout their + popover’s border where the arrow sits. */ + display: none; +} + +.tsf-gbc-tooltip-popover .components-popover__triangle-bg { + fill: var(--tsf-user-colors-bg-accent); +} + +.tsf-gbc-tooltip-popover .components-popover__triangle-border { + /* Hide the arrow stroke/border */ + display: none; +} + +.tsf-tooltip { + /* The popover component needs this to not be absolutely positioned so that the parent + element has size. */ + position: unset; +} + +.tsf-tooltip-text-wrap { + flex-shrink: 1; + max-width: 23em; + box-shadow: none; +} \ No newline at end of file diff --git a/lib/css/media.css b/lib/css/media.css index 3c22c89e..f417eaff 100644 --- a/lib/css/media.css +++ b/lib/css/media.css @@ -1,6 +1,12 @@ .tsf-image-notifications { line-height: 1; vertical-align: bottom; + + > .tsf-tooltip-wrap { + /* In Gutenberg, this ensures the wrap has the size of its contents, not sure why it’s + not required in the classic editor. */ + display: inline-block; + } } .tsf-set-image-button.button, diff --git a/lib/css/post.css b/lib/css/post.css index 157f2e57..5211bda4 100644 --- a/lib/css/post.css +++ b/lib/css/post.css @@ -218,18 +218,6 @@ body.js .tsf-flex-tab-content.tsf-flex-tab-content-active { line-height: 1em; } -/* Fix tooltip overflow blocking in the main tooltip area */ -.edit-post-meta-boxes-main, -.edit-post-meta-boxes-main .edit-post-meta-boxes-main__liner { - overflow: unset; - isolation: unset; -} - -/* Fix tooltip overflow blocking in the sidebar */ -.interface-complementary-area .components-panel { - position: unset; -} - /** * Start override */ diff --git a/lib/css/tt.css b/lib/css/tt.css index 0ba573e0..225eaa09 100644 --- a/lib/css/tt.css +++ b/lib/css/tt.css @@ -26,7 +26,7 @@ a.tsf-tooltip-item { pointer-events: none; box-sizing: border-box; display: flex; - flex: 1 1 auto; +/* flex: 1 1 auto;*/ /* Is this ever in a flex-container? */ flex-flow: row wrap; justify-content: flex-start; direction: ltr; @@ -46,8 +46,8 @@ a.tsf-tooltip-item { font-weight: 400; line-height: 1.625em; border-radius: 3px; - background: #424242; - color: #fbf7fd; + background-color: var( --tsf-user-colors-bg-accent ); + color: var( --tsf-user-colors-fg-accent ); box-shadow: 0px 0px 2px rgba( 0, 0, 0, .6 ); text-shadow: none; word-wrap: break-word; @@ -61,7 +61,7 @@ a.tsf-tooltip-item { } .tsf-tooltip-text-wrap * { - color: #fbf7fd; + color: var( --tsf-user-colors-fg-accent ); } .tsf-tooltip-text span { @@ -122,7 +122,7 @@ a.tsf-tooltip-item { content: ""; border-left: 8px solid transparent; border-right: 8px solid transparent; - border-top: 8px solid #424242; + border-top: 8px solid var( --tsf-user-colors-bg-accent ); position: absolute; bottom: 1px; left: 1px; @@ -139,5 +139,5 @@ a.tsf-tooltip-item { bottom: auto; top: 1px; border-top: 0; - border-bottom: 8px solid #424242; + border-bottom: 8px solid var( --tsf-user-colors-bg-accent ); } diff --git a/lib/js/gbc.js b/lib/js/gbc.js index 837b097a..2f2c06ac 100644 --- a/lib/js/gbc.js +++ b/lib/js/gbc.js @@ -293,6 +293,79 @@ window.tsfGBC = function( $ ) { ); } + function GBCTooltip( { element, desc, setTooltip, tt } ) { + const { createElement } = wp.element; + const { ttNames, ttSelectors } = tt; + + if ( ! desc ) return; + + const __html = `${desc}
`; + const hoverItemSuperWrap = element.closest( ttSelectors.superWrap ), + hoverItemWrap = element.closest( ttSelectors.wrap ) || element.parentElement; + + return createElement( wp.components.Popover, + { + // Key to force recreating the popover. Otherwise the popover’s + // rectangle is stale if an adjacent tooltip was displayed. + key: desc, + ref: ( popover ) => { + if ( ! popover ) return; // The element unmounted. + + setTooltip( popover ); + // Wait a frame so the rectangle of the popover is available. + requestAnimationFrame( () => { + const { x: popoverX } = popover.getBoundingClientRect(); + const { x: referenceX } = hoverItemWrap.getBoundingClientRect(); + const relativeX = popoverX - referenceX; + popover.dataset.adjust = relativeX; + } ); + }, + className: 'tsf-gbc-tooltip-popover', + anchor: hoverItemSuperWrap || hoverItemWrap, + animate: false, + shift: true, + resize: false, + variant: 'unstyled', + placement: 'top', + inline: true, + noArrow: false, + offset: 8, + }, + createElement( 'div', { + className: ttNames.base, + dangerouslySetInnerHTML: { __html }, + } ) + ); + } + + function overrideTooltip( base ) { + const rootNode = document.createElement( 'div' ); + rootNode.classList.add( 'tsf-gbc-tooltip-root' ); + const root = wp.element.createRoot( + document.body.appendChild( rootNode ) + ); + base.ttSelectors.arrow = '.components-popover__arrow'; + return { + _renderTooltip: async ( _event, element, desc ) => { + element.dataset.hasTooltip = 1; + const tooltipPromise = new Promise( ( resolve ) => { + const setTooltip = ( tooltip ) => resolve( tooltip ); + root.render( + wp.element.createElement( + GBCTooltip, + { element, desc, setTooltip, tt: base } + ) + ); + } ); + return await tooltipPromise; + }, + removeTooltip: ( element ) => { + root.render( null ); + base.removeTooltip( element ); + } + }; + } + /** * Initializes Gutenberg's compatibility and dispatches event hooks. * @@ -309,6 +382,9 @@ window.tsfGBC = function( $ ) { subscribe( saveDispatcher ); document.dispatchEvent( new CustomEvent( 'tsf-subscribed-to-gutenberg' ) ); + document.dispatchEvent( + new CustomEvent( 'tsf-gutenberg-tt', { detail: overrideTooltip } ) + ); } return Object.assign( { diff --git a/lib/js/media.js b/lib/js/media.js index 504fbbcf..40149bc9 100644 --- a/lib/js/media.js +++ b/lib/js/media.js @@ -687,9 +687,10 @@ window.tsfMedia = function () { if ( preview ) { if ( success ) { - // 250px is the max width for tooltips; we subtract 24 for padding, and 1 for subpixel rounding errors. - // We set min-height and width as that will prevent jumping. Also, those are the absolute-minimum for sharing/schema images. - imageObject.style = "max-width:225px;max-height:225px;min-width:60px;min-height:60px;border-radius:3px;display:block;"; + imageObject.style = "max-width:100%;max-height:100%;object-fit:contain;aspect-ratio:1.91;height:auto;border-radius:3px;display:block;"; + // Add dimensions to prevent jumping. + imageObject.width = imageObject.naturalWidth; + imageObject.height = imageObject.naturalHeight; preview.dataset.desc = imageObject.outerHTML; showElement( preview ); diff --git a/lib/js/tt.js b/lib/js/tt.js index 1c8a462a..a06de6d5 100644 --- a/lib/js/tt.js +++ b/lib/js/tt.js @@ -48,11 +48,15 @@ window.tsfTT = function () { // Yes, I'm too lazy to copy/paste whatever's above again to prepend a dot, so I spent half an hour figuring this. const ttSelectors = Object.fromEntries( Object.entries( ttNames ).map( ( [ i, v ] ) => [ i, `.${v}` ] ) ); + const ttMap = new Map(); + const _activeToolTipHandles = { updateDesc: event => { - if ( ! event.target.classList.contains( ttNames.item ) ) return; + if ( ! event.target.matches( ttSelectors.item ) ) return; + const tooltip = ttMap.get( event.target ); + if ( ! tooltip ) return; - let tooltipText = event.target.querySelector( ttSelectors.text ); + const tooltipText = tooltip.querySelector( ttSelectors.text ); if ( tooltipText instanceof Element ) { tooltipText.innerHTML = event.target.dataset.desc; event.target.dispatchEvent( new Event( 'mousemove' ) ); // event time: <.3ms. @@ -116,6 +120,19 @@ window.tsfTT = function () { wrap: void 0, reset: () => { _activeTooltipElements.tooltip = _activeTooltipElements.arrow = _activeTooltipElements.wrap = void 0; + }, + get: ( element ) => { + if ( ! _activeTooltipElements.tooltip ) { + if ( ttMap.size < 1 ) return {}; + + const tooltip = ttMap.get( element ); + Object.assign( _activeTooltipElements, { + tooltip, + arrow: tooltip.querySelector( ttSelectors.arrow ), + wrap: element.closest( ttSelectors.wrap ) || element.parentNode, + } ); + } + return _activeTooltipElements; } } @@ -170,7 +187,7 @@ window.tsfTT = function () { const event = _pointer.lastMoveEvent, element = event.target; - let tooltip = _activeTooltipElements.tooltip || ( element.querySelector( ttSelectors.base ) ); + const { tooltip, arrow, wrap } = _activeTooltipElements.get( element ); // Browser lagged, no tooltip exists (yet). Bail. if ( ! tooltip ) { @@ -178,10 +195,6 @@ window.tsfTT = function () { return; } - _activeTooltipElements.tooltip ||= tooltip; - _activeTooltipElements.arrow ||= tooltip.querySelector( ttSelectors.arrow ); - _activeTooltipElements.wrap ||= element.closest( ttSelectors.wrap ) || element.parentNode; - let pagex = _pointer.currPos.x; if ( 'focus' === event.type ) { @@ -189,17 +202,18 @@ window.tsfTT = function () { pagex = element.getBoundingClientRect().left + ( element.offsetWidth / 2 ); } else if ( isNaN( pagex ) ) { // Get the last known tooltip position on manual tooltip alteration. - pagex = _activeTooltipElements.tooltip.dataset.lastPagex || element.getBoundingClientRect().left; + pagex = tooltip.dataset.lastPagex || element.getBoundingClientRect().left; } // Keep separate record of pagex, so updateDesc() can utilize this via isNaN hereabove. - _activeTooltipElements.tooltip.dataset.lastPagex = pagex; + tooltip.dataset.lastPagex = pagex; - const textWrap = _activeTooltipElements.tooltip.querySelector( ttSelectors.textWrap ), + const textWrap = tooltip.querySelector( ttSelectors.textWrap ), arrowBoundary = 7, - arrowWidth = 16; + arrowWidth = 16, + wrapRect = wrap.getBoundingClientRect(); - let mousex = pagex - _activeTooltipElements.wrap.getBoundingClientRect().left - ( arrowWidth / 2 ), - adjust = _activeTooltipElements.tooltip.dataset.adjust, + let mousex = pagex - wrapRect.left - ( arrowWidth / 2 ), + adjust = tooltip.dataset.adjust, boundaryRight = textWrap.offsetWidth - arrowWidth - arrowBoundary; // mousex is skewed, adjust. @@ -210,7 +224,7 @@ window.tsfTT = function () { mousex = mousex - adjust; // Use textWidth for right boundary if adjustment exceeds. - if ( boundaryRight + adjust > _activeTooltipElements.wrap.offsetWidth ) { + if ( boundaryRight + adjust > wrapRect.right ) { let innerText = textWrap.querySelector( ttSelectors.text ), textWidth = innerText.offsetWidth; boundaryRight = textWidth - arrowWidth - arrowBoundary; @@ -219,13 +233,13 @@ window.tsfTT = function () { if ( mousex <= arrowBoundary ) { // Overflown left. - _activeTooltipElements.arrow.style.left = `${arrowBoundary}px`; + arrow.style.left = `${arrowBoundary}px`; } else if ( mousex >= boundaryRight ) { // Overflown right. - _activeTooltipElements.arrow.style.left = `${boundaryRight}px`; + arrow.style.left = `${boundaryRight}px`; } else { // Somewhere in the middle. - _activeTooltipElements.arrow.style.left = `${mousex}px`; + arrow.style.left = `${mousex}px`; } if ( isMouseEvent ) { @@ -460,7 +474,6 @@ window.tsfTT = function () { const boundary = element.closest( ttSelectors.boundary ) - || element.closest( '#tabs-1-edit-post\\/document-view' ) // Gutenberg sidebar WP 6.6+ || document.getElementById( 'wpcontent' ) || document.body; @@ -618,7 +631,7 @@ window.tsfTT = function () { tooltip.style.bottom = `${tooltipHeight - offsetTop}px`; } - return true; + return tooltip; } /** @@ -639,21 +652,26 @@ window.tsfTT = function () { * @param {string} desc The tooltip, may contain renderable HTML. * @return {Promise} True on success, false otherwise. */ - function doTooltip( event, element, desc ) { + async function doTooltip( event, element, desc ) { // Backward compatibility for jQuery vs ES. if ( element?.[0] ) element = element[0]; // Remove old tooltips, if any. - for ( const element of document.querySelectorAll( ttSelectors.base ) ) { + for ( const [ element ] of ttMap ) { removeTooltip( element ); _events( element ).unset(); } if ( ! desc.length ) return false; - return _renderTooltip( event, element, desc ); + ttMap.set( element, + // It’s async when Gutenberging. + await _renderTooltip( event, element, desc ) + ); + + return true; } /** @@ -676,7 +694,7 @@ window.tsfTT = function () { * @since 4.1.0 Now also clears the data of the tooltip. * @access public * - * @param {!jQuery|Element|string} element + * @param {!jQuery|Element} element */ function removeTooltip( element ) { @@ -690,7 +708,11 @@ window.tsfTT = function () { } const toolTip = getTooltip( element ); - toolTip?.parentNode.removeChild( toolTip ); + // Remove the tooltip directly only if contained. In Gutenberg, the override handles this. + if ( element.contains( toolTip ) ) { + toolTip?.parentNode.removeChild( toolTip ); + } + ttMap.delete( element ); } /** @@ -700,7 +722,7 @@ window.tsfTT = function () { * @since 4.2.0 Now returns a `HTMLElement` instead of a `jQuery.Element`. * @access public * - * @param {!jQuery|Element|string} element + * @param {!jQuery|Element} element * @return {(Element|undefined)} */ function getTooltip( element ) { @@ -709,9 +731,9 @@ window.tsfTT = function () { if ( element?.[0] ) element = element[0]; - return element?.classList.contains( ttNames.base ) - ? element - : element?.querySelector( ttSelectors.base ); + return ttMap.get( element ) || ttMap.get( + element?.querySelector( ttSelectors.base ) + ); } let _debounceTriggerReset = void 0; @@ -767,6 +789,11 @@ window.tsfTT = function () { */ load: () => { document.body.addEventListener( 'tsf-ready', _initToolTips ); + + document.addEventListener( 'tsf-gutenberg-tt', ( event ) => { + ( { _renderTooltip, removeTooltip } = + event.detail( { removeTooltip, ttNames, ttSelectors } ) ); + } ); }, }, { /**