diff --git a/assets/dev/js/admin/admin.js b/assets/dev/js/admin/admin.js index d40ca8338476..caa8025cf6eb 100644 --- a/assets/dev/js/admin/admin.js +++ b/assets/dev/js/admin/admin.js @@ -118,6 +118,22 @@ import FloatingButtonsHandler from 'elementor/modules/floating-buttons/assets/js } ); } ); + $( '.e-notice--cta.e-notice--dismissible[data-notice_id="plugin_image_optimization"] a.e-button--cta' ).on( 'click', function() { + elementorCommon.ajax.addRequest( 'elementor_image_optimization_campaign', { + data: { + source: 'io-wp-media-library-install', + }, + } ); + } ); + + $( '.e-a-apps .e-a-item[data-plugin="image-optimization/image-optimization.php"] a.e-btn' ).on( 'click', function() { + elementorCommon.ajax.addRequest( 'elementor_image_optimization_campaign', { + data: { + source: 'io-esetting-addons-install', + }, + } ); + } ); + $( '#elementor-clear-cache-button' ).on( 'click', function( event ) { event.preventDefault(); var $thisButton = $( this ); diff --git a/assets/dev/js/admin/hints/media.js b/assets/dev/js/admin/hints/media.js index 71ca854d583f..74261481f332 100644 --- a/assets/dev/js/admin/hints/media.js +++ b/assets/dev/js/admin/hints/media.js @@ -108,6 +108,7 @@ dismissId: event.target.closest( '.e-hint__container' ).dataset.event, }, } ); + this.hideHint( event ); }, diff --git a/assets/dev/js/editor/controls/gallery.js b/assets/dev/js/editor/controls/gallery.js index 641f97be3125..6b8ea94612da 100644 --- a/assets/dev/js/editor/controls/gallery.js +++ b/assets/dev/js/editor/controls/gallery.js @@ -258,6 +258,13 @@ ControlMediaItemView = ControlBaseDataView.extend( { if ( actionURL ) { window.open( actionURL, '_blank' ); } + + elementorCommon.ajax.addRequest( 'elementor_image_optimization_campaign', { + data: { + source: 'io-editor-gallery-install', + }, + } ); + this.hidePromotion(); }, diff --git a/assets/dev/js/editor/controls/media.js b/assets/dev/js/editor/controls/media.js index e16003dba521..8dedd3746369 100644 --- a/assets/dev/js/editor/controls/media.js +++ b/assets/dev/js/editor/controls/media.js @@ -221,6 +221,13 @@ ControlMediaItemView = ControlMultipleBaseItemView.extend( { if ( ! eventName ) { eventName = this.getDismissPromotionEventName(); } + + elementorCommon.ajax.addRequest( 'elementor_image_optimization_campaign', { + data: { + source: 'io-editor-image-install', + }, + } ); + // Prevent opening the same promotion again in current editor session. elementor.config.user.dismissed_editor_notices.push( eventName ); }, diff --git a/assets/dev/js/editor/utils/helpers.js b/assets/dev/js/editor/utils/helpers.js index f4cad891672f..7c96591b85f6 100644 --- a/assets/dev/js/editor/utils/helpers.js +++ b/assets/dev/js/editor/utils/helpers.js @@ -1,7 +1,7 @@ import ColorPicker from './color-picker'; import DocumentHelper from 'elementor-editor/document/helper-bc'; import ContainerHelper from 'elementor-editor-utils/container-helper'; -import DOMPurify from 'dompurify'; +import DOMPurify, { isValidAttribute } from 'dompurify'; const allowedHTMLWrapperTags = [ 'article', @@ -704,4 +704,10 @@ module.exports = { sanitize( value, options ) { return DOMPurify.sanitize( value, options ); }, + + sanitizeUrl( url ) { + const isValidUrl = !! url ? isValidAttribute( 'a', 'href', url ) : false; + + return isValidUrl ? url : ''; + }, }; diff --git a/core/admin/admin.php b/core/admin/admin.php index b629b9b409b1..bcbbbebe068d 100644 --- a/core/admin/admin.php +++ b/core/admin/admin.php @@ -878,6 +878,8 @@ public function __construct() { add_action( 'in_plugin_update_message-' . ELEMENTOR_PLUGIN_BASE, function( $plugin_data ) { $this->version_update_warning( ELEMENTOR_VERSION, $plugin_data['new_version'] ); } ); + + add_action( 'elementor/ajax/register_actions', [ $this, 'register_ajax_hints' ] ); } /** @@ -987,4 +989,22 @@ private function maybe_enqueue_hints() { wp_enqueue_script( 'media-hints' ); } + + public function register_ajax_hints( $ajax_manager ) { + $ajax_manager->register_ajax_action( 'elementor_image_optimization_campaign', [ $this, 'ajax_set_image_optimization_campaign' ] ); + } + + public function ajax_set_image_optimization_campaign( $request ) { + if ( empty( $request['source'] ) ) { + return; + } + + $campaign_data = [ + 'source' => sanitize_key( $request['source'] ), + 'campaign' => 'io-plg', + 'medium' => 'wp-dash', + ]; + + set_transient( 'elementor_image_optimization_campaign', $campaign_data, 30 * DAY_IN_SECONDS ); + } } diff --git a/includes/widgets/heading.php b/includes/widgets/heading.php index 609ea9bf3ffc..d2063050e894 100644 --- a/includes/widgets/heading.php +++ b/includes/widgets/heading.php @@ -390,7 +390,7 @@ protected function content_template() { let title = elementor.helpers.sanitize( settings.title, { ALLOW_DATA_ATTR: false } ); if ( '' !== settings.link.url ) { - title = '' + title + ''; + title = '' + title + ''; } view.addRenderAttribute( 'title', 'class', [ 'elementor-heading-title' ] ); diff --git a/includes/widgets/icon-box.php b/includes/widgets/icon-box.php index be764910d25c..1b4316de9508 100644 --- a/includes/widgets/icon-box.php +++ b/includes/widgets/icon-box.php @@ -789,7 +789,7 @@ protected function content_template() { view.addRenderAttribute( 'icon', 'class', 'elementor-icon elementor-animation-' + settings.hover_animation ); if ( hasLink ) { - view.addRenderAttribute( 'link', 'href', settings.link.url ); + view.addRenderAttribute( 'link', 'href', elementor.helpers.sanitizeUrl( settings.link.url ) ); view.addRenderAttribute( 'icon', 'tabindex', '-1' ); } diff --git a/includes/widgets/icon-list.php b/includes/widgets/icon-list.php index a4a511da93ea..658802b95536 100644 --- a/includes/widgets/icon-list.php +++ b/includes/widgets/icon-list.php @@ -806,7 +806,7 @@ protected function content_template() {
  • <# if ( item.link && item.link.url ) { #> - + <# } #> <# if ( item.icon || item.selected_icon.value ) { #> diff --git a/includes/widgets/icon.php b/includes/widgets/icon.php index f085828aabec..b8965fb4b541 100644 --- a/includes/widgets/icon.php +++ b/includes/widgets/icon.php @@ -486,7 +486,7 @@ protected function content_template() { return; } - const link = settings.link.url ? 'href="' + _.escape( settings.link.url ) + '"' : '', + const link = settings.link.url ? 'href="' + elementor.helpers.sanitizeUrl( settings.link.url ) + '"' : '', iconHTML = elementor.helpers.renderIcon( view, settings.selected_icon, { 'aria-hidden': true }, 'i' , 'object' ), migrated = elementor.helpers.isIconMigrated( settings, 'selected_icon' ), iconTag = link ? 'a' : 'div'; diff --git a/includes/widgets/image-box.php b/includes/widgets/image-box.php index 4d89788353a4..8ccbaa0bf9b4 100644 --- a/includes/widgets/image-box.php +++ b/includes/widgets/image-box.php @@ -700,7 +700,7 @@ protected function content_template() { var imageHtml = ''; if ( settings.link.url ) { - imageHtml = '' + imageHtml + ''; + imageHtml = '' + imageHtml + ''; } html += '
    ' + imageHtml + '
    '; @@ -714,7 +714,7 @@ protected function content_template() { titleSizeTag = elementor.helpers.validateHTMLTag( settings.title_size ); if ( settings.link.url ) { - title_html = '' + title_html + ''; + title_html = '' + title_html + ''; } view.addRenderAttribute( 'title_text', 'class', 'elementor-image-box-title' ); diff --git a/includes/widgets/image.php b/includes/widgets/image.php index 548dbe428719..617ed6a295c0 100644 --- a/includes/widgets/image.php +++ b/includes/widgets/image.php @@ -822,7 +822,7 @@ protected function content_template() { } if ( link_url ) { - #><# + #><# } #><# diff --git a/includes/widgets/social-icons.php b/includes/widgets/social-icons.php index 8c5215446950..2e39b4d84c66 100644 --- a/includes/widgets/social-icons.php +++ b/includes/widgets/social-icons.php @@ -660,7 +660,7 @@ protected function content_template() { social = elementor.helpers.getSocialNetworkNameFromIcon( item.social_icon, item.social, false, migrated ); #> - + {{{ social }}} <# iconsHTML[ index ] = elementor.helpers.renderIcon( view, item.social_icon, {}, 'i', 'object' ); diff --git a/includes/widgets/testimonial.php b/includes/widgets/testimonial.php index 86e697949376..6f4e289dd77d 100644 --- a/includes/widgets/testimonial.php +++ b/includes/widgets/testimonial.php @@ -567,7 +567,7 @@ protected function content_template() { var imageHtml = 'testimonial'; if ( settings.link.url ) { - imageHtml = '' + imageHtml + ''; + imageHtml = '' + imageHtml + ''; } } diff --git a/includes/widgets/traits/button-trait.php b/includes/widgets/traits/button-trait.php index a1437f99a9d9..6ce3a704b852 100644 --- a/includes/widgets/traits/button-trait.php +++ b/includes/widgets/traits/button-trait.php @@ -577,7 +577,7 @@ protected function content_template() { view.addRenderAttribute( 'button', 'class', 'elementor-button' ); if ( '' !== settings.link.url ) { - view.addRenderAttribute( 'button', 'href', settings.link.url ); + view.addRenderAttribute( 'button', 'href', elementor.helpers.sanitizeUrl( settings.link.url ) ); view.addRenderAttribute( 'button', 'class', 'elementor-button-link' ); } else { view.addRenderAttribute( 'button', 'role', 'button' ); diff --git a/modules/ai/assets/js/editor/api/index.js b/modules/ai/assets/js/editor/api/index.js index dad5bd9a17da..60bf80182b31 100644 --- a/modules/ai/assets/js/editor/api/index.js +++ b/modules/ai/assets/js/editor/api/index.js @@ -1,6 +1,10 @@ const request = ( endpoint, data = {}, immediately = false, signal ) => { if ( Object.keys( data ).length ) { - data.context = window.elementorAiCurrentContext; + if ( window.elementorAiCurrentContext ) { + data.context = window.elementorAiCurrentContext; + } else { + data.context = window.elementorWpAiCurrentContext; + } } return new Promise( ( resolve, reject ) => { diff --git a/modules/ai/assets/js/gutenberg/edit-text-with-ai.js b/modules/ai/assets/js/gutenberg/edit-text-with-ai.js new file mode 100644 index 000000000000..8187acf80e61 --- /dev/null +++ b/modules/ai/assets/js/gutenberg/edit-text-with-ai.js @@ -0,0 +1,47 @@ +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { AiText } from './text-with-ai'; +import { AIIcon } from '@elementor/icons'; +import React from 'react'; +const { ToolbarButton } = wp.components; +const { BlockControls } = wp.blockEditor; + +export const EditTextWithAi = ( props ) => { + const [ shouldRenderAiApp, setShouldRenderAiApp ] = useState( false ); + const BlockEdit = props.blockEdit; + + const supportedBlocks = [ 'core/paragraph', 'core/heading' ]; + + if ( ! supportedBlocks.includes( props.name ) ) { + return ; + } + + if ( shouldRenderAiApp ) { + return setShouldRenderAiApp( false ) } + blockName={ props.name } + initialValue={ props.attributes.content ? String( props.attributes.content ) : '' } + blockClientId={ props.clientId } + />; + } + + return ( + <> + + } + label={ __( 'Edit with Elementor AI', 'elementor' ) } + onClick={ () => setShouldRenderAiApp( true ) } + /> + + { } + + ); +}; + +EditTextWithAi.propTypes = { + name: PropTypes.string, + blockEdit: PropTypes.func, + attributes: PropTypes.object, + clientId: PropTypes.string, +}; + diff --git a/modules/ai/assets/js/gutenberg/excerpt.js b/modules/ai/assets/js/gutenberg/excerpt.js index 03fdcd58db7c..e261fd5343a5 100644 --- a/modules/ai/assets/js/gutenberg/excerpt.js +++ b/modules/ai/assets/js/gutenberg/excerpt.js @@ -26,7 +26,7 @@ const GenerateExcerptWithAI = () => { }; return ( -
    +
    { __( 'Generate with Elementor AI', 'elementor' ) } diff --git a/modules/ai/assets/js/gutenberg/index.js b/modules/ai/assets/js/gutenberg/index.js index 6ad738960fdb..d73ae6d7de00 100644 --- a/modules/ai/assets/js/gutenberg/index.js +++ b/modules/ai/assets/js/gutenberg/index.js @@ -2,6 +2,8 @@ import { createRoot } from '@wordpress/element'; import GenerateExcerptWithAI from './excerpt'; import GenerateFeaturedImageWithAI from './featured-image'; import { GenerateTextWithAi } from './text-with-ai'; +import React from 'react'; +import { EditTextWithAi } from './edit-text-with-ai'; ( function() { 'use strict'; @@ -16,16 +18,7 @@ import { GenerateTextWithAi } from './text-with-ai'; if ( excerptPanel && ! document.querySelector( '.e-excerpt-ai' ) ) { const rootElement = document.createElement( 'div' ); rootElement.classList.add( 'e-excerpt-ai' ); - // Find the existing link with class "components-external-link" - const existingExcerptLink = excerptPanel.querySelector( '.components-external-link' ); - if ( existingExcerptLink ) { - existingExcerptLink.classList.add( 'existing-excerpt-link' ); - // Append the new link before the existing one - excerptPanel.insertBefore( rootElement, existingExcerptLink ); - } else { - // Append the new link to the excerpt panel - excerptPanel.appendChild( rootElement ); - } + excerptPanel.appendChild( rootElement ); const urlSearchParams = new URLSearchParams( window.location.search ); elementorCommon?.ajax?.addRequestConstant( 'editor_post_id', urlSearchParams?.get( 'post' ) ); @@ -48,14 +41,16 @@ import { GenerateTextWithAi } from './text-with-ai'; } }; - const addTextWithAI = ( blockName ) => { - const textPanel = document.querySelector( '.block-editor-block-card__content' ); - if ( textPanel && ! document.querySelector( '.e-text-ai' ) ) { + const addTextWithAI = ( blockName, blockClientId ) => { + const textPanel = document.querySelector( '.block-editor-block-card__description, .block-editor-block-card__content' ); + if ( textPanel && ! document.querySelector( `.e-text-ai[data-client-id="${ blockClientId }"]` ) ) { + removeAiIndicator(); const rootElement = document.createElement( 'div' ); rootElement.classList.add( 'e-text-ai' ); + rootElement.setAttribute( 'data-client-id', blockClientId ); textPanel.appendChild( rootElement ); const root = createRoot( rootElement ); - root.render( ); + root.render( ); } }; @@ -78,7 +73,7 @@ import { GenerateTextWithAi } from './text-with-ai'; const addAiIndicatorToTextBlock = ( blockNames ) => { const selectedBlock = wp.data.select( 'core/block-editor' )?.getSelectedBlock(); if ( selectedBlock && blockNames.some( ( name ) => selectedBlock.name.includes( name ) ) ) { - addTextWithAI( selectedBlock.name ); + addTextWithAI( selectedBlock.name, selectedBlock.clientId ); } else { removeAiIndicator(); } @@ -89,5 +84,57 @@ import { GenerateTextWithAi } from './text-with-ai'; addAiIndicator( 'featured-image', addGenerateFeaturedImageWithAI ); addAiIndicatorToTextBlock( [ 'paragraph', 'heading' ] ); } ); + + const observer = new MutationObserver( ( mutationsList ) => { + for ( const mutation of mutationsList ) { + if ( 'childList' === mutation.type ) { + if ( document.querySelector( '.editor-post-excerpt' ) ) { + addGenerateExcerptWithAI(); + } + } + } + } ); + + observer.observe( document.body, { childList: true, subtree: true } ); + window.addEventListener( 'beforeunload', () => { + observer.disconnect(); + } ); } ); } )( jQuery ); + +( function( wp ) { + const { addFilter } = wp.hooks; + + const addAiButtonToToolbar = ( BlockEdit ) => { + return ( props ) => { + return ; + }; + }; + + addFilter( 'editor.BlockEdit', 'elementor-ai-toolbar-button', addAiButtonToToolbar ); +} )( window.wp ); + +( function() { + 'use strict'; + + const setElementorWpAiCurrentContext = () => { + const selectedBlock = wp.data.select( 'core/block-editor' ).getSelectedBlock(); + if ( selectedBlock ) { + const blockName = 'core/heading' === selectedBlock.name ? 'heading' : selectedBlock.name; + window.elementorWpAiCurrentContext = { + widgetType: blockName, + controlName: blockName, + }; + } else { + window.elementorWpAiCurrentContext = null; + } + }; + + wp.data.subscribe( setElementorWpAiCurrentContext ); + + const clearElementorAiCurrentContext = () => { + window.elementorWpAiCurrentContext = null; + }; + + window.addEventListener( 'beforeunload', clearElementorAiCurrentContext ); +} )(); diff --git a/modules/ai/assets/js/gutenberg/text-with-ai.js b/modules/ai/assets/js/gutenberg/text-with-ai.js index 7cab01e3c1a3..9649237a530e 100644 --- a/modules/ai/assets/js/gutenberg/text-with-ai.js +++ b/modules/ai/assets/js/gutenberg/text-with-ai.js @@ -9,7 +9,7 @@ import PropTypes from 'prop-types'; const { useDispatch, useSelect } = wp.data; const { createBlock } = wp.blocks; -const AiText = ( { onClose, blockName } ) => { +export const AiText = ( { onClose, blockName, initialValue = '', blockClientId = '' } ) => { const { replaceBlocks, insertBlocks } = useDispatch( 'core/block-editor' ); const insertTextIntoParagraph = ( text ) => { if ( paragraphBlock ) { @@ -31,7 +31,7 @@ const AiText = ( { onClose, blockName } ) => { const { paragraphBlock } = useSelect( ( ) => { const currentBlocks = wp.data.select( 'core/block-editor' )?.getBlocks(); - const foundParagraphBlock = currentBlocks.find( ( block ) => blockName === block.name ); + const foundParagraphBlock = currentBlocks.find( ( block ) => blockName === block.name && blockClientId === block.clientId ); return { blocks: currentBlocks, paragraphBlock: foundParagraphBlock, @@ -44,7 +44,9 @@ const AiText = ( { onClose, blockName } ) => { <> {} } + getControlValue={ () => { + return initialValue; + } } setControlValue={ ( value ) => { insertTextIntoParagraph( value ); } } @@ -59,9 +61,11 @@ const AiText = ( { onClose, blockName } ) => { AiText.propTypes = { onClose: PropTypes.func.isRequired, blockName: PropTypes.string.isRequired, + initialValue: PropTypes.string, + blockClientId: PropTypes.string, }; -export const GenerateTextWithAi = ( { blockName } ) => { +export const GenerateTextWithAi = ( { blockName, blockClientId } ) => { const [ isOpen, setIsOpen ] = useState( false ); const handleButtonClick = () => { @@ -77,11 +81,12 @@ export const GenerateTextWithAi = ( { blockName } ) => { { __( 'Generate with Elementor AI', 'elementor' ) } - { isOpen && } + { isOpen && }
    ); }; GenerateTextWithAi.propTypes = { blockName: PropTypes.string.isRequired, + blockClientId: PropTypes.string.isRequired, }; diff --git a/modules/ai/module.php b/modules/ai/module.php index a3f8992accd3..5ace0cb4eeff 100644 --- a/modules/ai/module.php +++ b/modules/ai/module.php @@ -119,6 +119,10 @@ public function __construct() { 'wp-element', 'wp-editor', 'wp-data', + 'wp-components', + 'wp-compose', + 'wp-i18n', + 'wp-hooks', ], ELEMENTOR_VERSION, true ); diff --git a/modules/apps/admin-apps-page.php b/modules/apps/admin-apps-page.php index fbb3b3ab9141..a78c040ba847 100644 --- a/modules/apps/admin-apps-page.php +++ b/modules/apps/admin-apps-page.php @@ -163,7 +163,7 @@ private static function is_elementor_pro_installed() { private static function render_plugin_item( $plugin ) { ?> -
    +
    >
    <?php echo esc_attr( $plugin['name'] ); ?> diff --git a/modules/floating-buttons/assets/scss/widgets/contact-buttons-var-10.scss b/modules/floating-buttons/assets/scss/widgets/contact-buttons-var-10.scss index 00e85a9dda48..b24891069aa0 100644 --- a/modules/floating-buttons/assets/scss/widgets/contact-buttons-var-10.scss +++ b/modules/floating-buttons/assets/scss/widgets/contact-buttons-var-10.scss @@ -111,6 +111,16 @@ &.has-size-small { --e-contact-buttons-margin-offset: 2px; } + + [dir="rtl"] & { + --e-contact-buttons-margin-offset: 2px; + + transform: translateX(calc(100% - calc(var(--e-contact-buttons-svg-size) * 2 - var(--e-contact-buttons-margin-offset)))); + + &.has-size-large { + --e-contact-buttons-margin-offset: 8px; + } + } } &__contact-links { @@ -154,6 +164,20 @@ transform: translateX(calc(100% - calc(var(--e-contact-buttons-svg-size) * 2) + var(--e-contact-buttons-margin-offset))); transition: var(--e-contact-buttons-transition); + &.has-size-small { + --e-contact-buttons-margin-offset: 2px; + } + + [dir="rtl"] & { + --e-contact-buttons-margin-offset: 2px; + + transform: translateX(calc(-100% + calc(var(--e-contact-buttons-svg-size) * 2 - var(--e-contact-buttons-margin-offset)))); + + &.has-size-large { + --e-contact-buttons-margin-offset: 8px; + } + } + &:hover, &:focus { @@ -167,10 +191,6 @@ transform: none; transition: var(--e-contact-buttons-transition); } - - &.has-size-small { - --e-contact-buttons-margin-offset: 2px; - } } &__contact-links {