first commit

This commit is contained in:
Ryan Ariana
2024-05-06 11:04:37 +07:00
commit aee061ddba
7322 changed files with 2918816 additions and 0 deletions

View File

@@ -0,0 +1,283 @@
<?php
namespace ElementorPro\Core\Admin;
use Elementor\Core\Base\App;
use Elementor\Rollback;
use Elementor\Settings;
use Elementor\Tools;
use Elementor\Utils;
use ElementorPro\Core\Utils as ProUtils;
use ElementorPro\License\API;
use ElementorPro\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Admin extends App {
/**
* Get module name.
*
* Retrieve the module name.
*
* @since 2.3.0
* @access public
*
* @return string Module name.
*/
public function get_name() {
return 'admin';
}
/**
* Enqueue admin styles.
*
* @since 1.0.0
* @return void
*/
public function enqueue_styles() {
$suffix = Utils::is_script_debug() ? '' : '.min';
$direction_suffix = is_rtl() ? '-rtl' : '';
wp_register_style(
'elementor-pro-admin',
ELEMENTOR_PRO_ASSETS_URL . 'css/admin' . $direction_suffix . $suffix . '.css',
[],
ELEMENTOR_PRO_VERSION
);
wp_enqueue_style( 'elementor-pro-admin' );
}
public function enqueue_scripts() {
$suffix = Utils::is_script_debug() ? '' : '.min';
wp_enqueue_script(
'elementor-pro-admin',
ELEMENTOR_PRO_URL . 'assets/js/admin' . $suffix . '.js',
[
'elementor-admin',
],
ELEMENTOR_PRO_VERSION,
true
);
$locale_settings = [];
/**
* Localized admin settings.
*
* Filters the localized settings used in the admin as JavaScript variables.
*
* By default Elementor Pro passes some admin settings to be consumed as JavaScript
* variables. This hook allows developers to add extra settings values to be consumed
* using JavaScript in WordPress admin.
*
* @since 1.0.0
*
* @param array $locale_settings Localized settings.
*/
$locale_settings = apply_filters( 'elementor_pro/admin/localize_settings', $locale_settings );
Utils::print_js_config(
'elementor-pro-admin',
'ElementorProConfig',
$locale_settings
);
}
public function remove_go_pro_menu() {
remove_action( 'admin_menu', [ Plugin::elementor()->settings, 'register_pro_menu' ], Settings::MENU_PRIORITY_GO_PRO );
}
private function get_rollback_versions() {
$rollback_versions = get_transient( 'elementor_pro_rollback_versions_' . ELEMENTOR_PRO_VERSION );
if ( false === $rollback_versions ) {
$max_versions = 30;
$versions = apply_filters( 'elementor-pro/settings/rollback/versions', [] );
if ( empty( $versions ) ) {
$versions = API::get_previous_versions();
if ( is_wp_error( $versions ) ) {
return [];
}
}
$rollback_versions = [];
$current_index = 0;
foreach ( $versions as $version ) {
if ( $max_versions <= $current_index ) {
break;
}
$lowercase_version = strtolower( $version );
$is_valid_rollback_version = ! preg_match( '/(trunk|beta|rc|dev)/i', $lowercase_version );
/**
* Is valid rollback version.
*
* Filters whether the version of the rollback is valid or not.
*
* By default Elementor doesn't allow to rollback for trunk/beta/rc/dev versions.
* This hook allows developers to enable a rollback for thise kind of versions by
* returning `true`.
*
* @param bool $is_valid_rollback_version Whether a rollback version is valid.
* @param array $lowercase_version A list of previous versions.
*/
$is_valid_rollback_version = apply_filters(
'elementor-pro/settings/tools/rollback/is_valid_rollback_version',
$is_valid_rollback_version,
$lowercase_version
);
if ( ! $is_valid_rollback_version ) {
continue;
}
if ( version_compare( $version, ELEMENTOR_VERSION, '>=' ) ) {
continue;
}
$current_index++;
$rollback_versions[] = $version;
}
set_transient( 'elementor_pro_rollback_versions_' . ELEMENTOR_PRO_VERSION, $rollback_versions, WEEK_IN_SECONDS );
}
return $rollback_versions;
}
public function register_admin_tools_fields( Tools $tools ) {
$rollback_html = '<select class="elementor-rollback-select">';
foreach ( $this->get_rollback_versions() as $version ) {
$rollback_html .= "<option value='{$version}'>$version</option>";
}
$rollback_html .= '</select>';
// Rollback
$tools->add_fields( 'versions', 'rollback', [
'rollback_pro_separator' => [
'field_args' => [
'type' => 'raw_html',
'html' => '<hr>',
],
],
'rollback_pro' => [
'label' => esc_html__( 'Rollback Pro Version', 'elementor-pro' ),
'field_args' => [
'type' => 'raw_html',
'html' => sprintf(
$rollback_html . '<a data-placeholder-text="' . esc_html__( 'Reinstall', 'elementor-pro' ) . ' v{VERSION}" href="#" data-placeholder-url="%s" class="button elementor-button-spinner elementor-rollback-button">%s</a>',
wp_nonce_url( admin_url( 'admin-post.php?action=elementor_pro_rollback&version=VERSION' ), 'elementor_pro_rollback' ),
__( 'Reinstall', 'elementor-pro' )
),
'desc' => '<span style="color: red;">' . esc_html__( 'Warning: Please backup your database before making the rollback.', 'elementor-pro' ) . '</span>',
],
],
] );
}
public function post_elementor_pro_rollback() {
check_admin_referer( 'elementor_pro_rollback' );
$rollback_versions = $this->get_rollback_versions();
$version = ProUtils::_unstable_get_super_global_value( $_GET, 'version' );
if ( ! $version || ! in_array( $version, $rollback_versions, true ) ) {
wp_die( esc_html__( 'Error occurred, The version selected is invalid. Try selecting different version.', 'elementor-pro' ) );
}
/**
* Filter to allow override the rollback process.
* Should return an instance of `Rollback` class.
*
* @since 3.16.0
*
* @param Rollback|null $rollback The rollback instance.
* @param string $version The version to roll back to.
*/
$rollback = apply_filters( 'elementor-pro/settings/rollback', null, $version );
if ( ! ( $rollback instanceof Rollback ) ) {
$package_url = API::get_plugin_package_url( $version );
if ( is_wp_error( $package_url ) ) {
wp_die( $package_url ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
$rollback = new Rollback( [
'version' => $version,
'plugin_name' => ELEMENTOR_PRO_PLUGIN_BASE,
'plugin_slug' => basename( ELEMENTOR_PRO__FILE__, '.php' ),
'package_url' => $package_url,
] );
}
$rollback->run();
wp_die( '', esc_html__( 'Rollback to Previous Version', 'elementor-pro' ), [ 'response' => 200 ] );
}
public function plugin_action_links( $links ) {
unset( $links['go_pro'] );
return $links;
}
public function plugin_row_meta( $plugin_meta, $plugin_file ) {
if ( ELEMENTOR_PRO_PLUGIN_BASE === $plugin_file ) {
$row_meta = [
'changelog' => '<a href="https://go.elementor.com/pro-changelog/" title="' . esc_attr( esc_html__( 'View Elementor Pro Changelog', 'elementor-pro' ) ) . '" target="_blank">' . esc_html__( 'Changelog', 'elementor-pro' ) . '</a>',
];
$plugin_meta = array_merge( $plugin_meta, $row_meta );
}
return $plugin_meta;
}
public function add_finder_items( array $categories ) {
$settings_url = Settings::get_url();
$categories['settings']['items']['integrations'] = [
'title' => esc_html__( 'Integrations', 'elementor-pro' ),
'icon' => 'integration',
'url' => $settings_url . '#tab-integrations',
'keywords' => [ 'integrations', 'settings', 'typekit', 'facebook', 'recaptcha', 'mailchimp', 'drip', 'activecampaign', 'getresponse', 'convertkit', 'elementor' ],
];
return $categories;
}
/**
* Admin constructor.
*/
public function __construct() {
$this->add_component( 'canary-deployment', new Canary_Deployment() );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_styles' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
add_action( 'admin_menu', [ $this, 'remove_go_pro_menu' ], 0 );
add_action( 'elementor/admin/after_create_settings/' . Tools::PAGE_ID, [ $this, 'register_admin_tools_fields' ], 50 );
add_filter( 'plugin_action_links_' . ELEMENTOR_PLUGIN_BASE, [ $this, 'plugin_action_links' ], 50 );
add_filter( 'plugin_row_meta', [ $this, 'plugin_row_meta' ], 10, 2 );
add_filter( 'elementor/finder/categories', [ $this, 'add_finder_items' ] );
add_action( 'admin_post_elementor_pro_rollback', [ $this, 'post_elementor_pro_rollback' ] );
add_action( 'in_plugin_update_message-' . ELEMENTOR_PRO_PLUGIN_BASE, function( $plugin_data ) {
Plugin::elementor()->admin->version_update_warning( ELEMENTOR_PRO_VERSION, $plugin_data['new_version'] );
} );
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace ElementorPro\Core\Admin;
use ElementorPro\License\API;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Canary_Deployment extends \Elementor\Core\Admin\Canary_Deployment {
const CURRENT_VERSION = ELEMENTOR_PRO_VERSION;
const PLUGIN_BASE = ELEMENTOR_PRO_PLUGIN_BASE;
protected function get_canary_deployment_remote_info( $force ) {
$version_info = API::get_version( false );
$canary_info = [];
if ( ! is_wp_error( $version_info ) && ! empty( $version_info['canary_deployment'] ) ) {
$canary_info = $version_info['canary_deployment'];
}
return $canary_info;
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace ElementorPro\Core\App;
use Elementor\Core\Base\App as BaseApp;
use ElementorPro\Plugin;
use ElementorPro\Core\App\Modules\SiteEditor\Module as SiteEditor;
use ElementorPro\Core\App\Modules\KitLibrary\Module as KitLibrary;
use ElementorPro\Core\App\Modules\Onboarding\Module as Onboarding;
use ElementorPro\Core\App\Modules\ImportExport\Module as ImportExport;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class App extends BaseApp {
/**
* Get module name.
*
* Retrieve the module name.
*
* @since 3.0.0
* @access public
*
* @return string Module name.
*/
public function get_name() {
return 'app-pro';
}
public function init() {
$this->enqueue_assets();
}
public function set_menu_url() {
Plugin::elementor()->app->set_settings( 'menu_url', Plugin::elementor()->app->get_base_url() . '#/site-editor' );
}
protected function get_init_settings() {
return [
'baseUrl' => $this->get_assets_base_url(),
];
}
protected function get_assets_base_url() {
return ELEMENTOR_PRO_URL;
}
private function enqueue_assets() {
wp_enqueue_style(
'elementor-pro-app',
$this->get_css_assets_url( 'app', null, 'default', true ),
[
'elementor-app',
'select2',
],
ELEMENTOR_VERSION
);
wp_enqueue_script(
'elementor-pro-app',
$this->get_js_assets_url( 'app' ),
[
'wp-i18n',
'elementor-app-packages',
'elementor-common',
'select2',
],
ELEMENTOR_PRO_VERSION,
true
);
wp_set_script_translations( 'elementor-pro-app', 'elementor-pro' );
}
private function enqueue_config() {
// If script didn't loaded, config is still relevant, enqueue without a file.
if ( ! wp_script_is( 'elementor-pro-app' ) ) {
wp_register_script( 'elementor-pro-app', false, [], ELEMENTOR_PRO_VERSION );
wp_enqueue_script( 'elementor-pro-app' );
}
$this->print_config( 'elementor-pro-app' );
}
public function __construct() {
$this->add_component( 'site-editor', new SiteEditor() );
$this->add_component( 'kit-library', new KitLibrary() );
$this->add_component( 'onboarding', new Onboarding() );
$this->add_component( 'import-export', new ImportExport() );
add_action( 'elementor/app/init', [ $this, 'init' ] );
add_action( 'elementor/common/after_register_scripts', function () {
$this->enqueue_config();
} );
add_action( 'elementor/init', [ $this, 'set_menu_url' ] );
}
}

View File

@@ -0,0 +1,22 @@
import ConnectButtonUI from '../ui/connect-button';
import { htmlDecodeTextContent, replaceUtmPlaceholders } from '../utils';
export default function useFeatureLock( featureName ) {
const appConfig = elementorAppProConfig[ featureName ] ?? {},
isLocked = appConfig.lock?.is_locked ?? false;
const buttonText = htmlDecodeTextContent( appConfig.lock?.button.text );
const buttonLink = replaceUtmPlaceholders(
appConfig.lock?.button.url ?? '',
appConfig.utms ?? {},
);
const ConnectButton = () => (
<ConnectButtonUI text={ buttonText } url={ buttonLink } />
);
return {
isLocked,
ConnectButton,
};
}

View File

@@ -0,0 +1,3 @@
import Module from '../../modules/site-editor/assets/js/site-editor';
new Module();

View File

@@ -0,0 +1,48 @@
import * as React from 'react';
import { useRef, useEffect } from 'react';
import { Button } from '@elementor/app-ui';
import { arrayToClassName } from '../utils.js';
const ConnectButton = ( props ) => {
const className = arrayToClassName( [
'e-app-connect-button',
props.className,
] );
const buttonRef = useRef( null );
useEffect( () => {
if ( ! buttonRef.current ) {
return;
}
jQuery( buttonRef.current ).elementorConnect();
}, [] );
return (
<Button
{ ...props }
elRef={ buttonRef }
className={ className }
/>
);
};
ConnectButton.propTypes = {
...Button.propTypes,
text: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
className: PropTypes.string,
};
ConnectButton.defaultProps = {
className: '',
variant: 'contained',
size: 'sm',
color: 'cta',
target: '_blank',
rel: 'noopener noreferrer',
text: __( 'Connect & Activate', 'elementor' ),
};
export default React.memo( ConnectButton );

View File

@@ -0,0 +1,29 @@
// Copied from Core.
export const arrayToClassName = ( array, action ) => {
return array
.filter( ( item ) => 'object' === typeof ( item ) ? Object.entries( item )[ 0 ][ 1 ] : item )
.map( ( item ) => {
const value = 'object' === typeof ( item ) ? Object.entries( item )[ 0 ][ 0 ] : item;
return action ? action( value ) : value;
} )
.join( ' ' );
};
export const htmlDecodeTextContent = ( input ) => {
const doc = new DOMParser().parseFromString( input, 'text/html' );
return doc.documentElement.textContent;
};
export const replaceUtmPlaceholders = ( link = '', utms = {} ) => {
if ( ! link || ! utms ) {
return link;
}
Object.keys( utms ).forEach( ( key ) => {
const match = new RegExp( `%%${ key }%%`, 'g' );
link = link.replace( match, utms[ key ] );
} );
return link;
};

View File

@@ -0,0 +1,8 @@
@import "../../modules/site-editor/assets/js/molecules/site-template.scss";
@import "../../modules/site-editor/assets/js/pages/add-new.scss";
@import "../../modules/site-editor/assets/js/pages/template-type.scss";
@import "../../modules/site-editor/assets/js/pages/conditions/conditions.scss";
@import "../../modules/site-editor/assets/js/molecules/back-button.scss";
@import "../../modules/site-editor/assets/js/atoms/indicator-bullet.scss";
@import "../../modules/site-editor/assets/js/atoms/preview-iframe.scss";
@import "../../modules/site-editor/assets/js/site-editor.scss";

View File

@@ -0,0 +1,70 @@
<?php
namespace ElementorPro\Core\App\Modules\ImportExport;
use Elementor\Core\Base\Module as BaseModule;
use ElementorPro\Plugin;
use ElementorPro\Modules\ThemeBuilder\Module as ThemeBuilderModule;
use Elementor\App\Modules\ImportExport\Processes\Export;
use Elementor\App\Modules\ImportExport\Processes\Import;
use Elementor\App\Modules\ImportExport\Processes\Revert;
use ElementorPro\Core\App\Modules\ImportExport\Runners\Import\Templates as ImportTemplates;
use ElementorPro\Core\App\Modules\ImportExport\Runners\Export\Templates as ExportTemplates;
use ElementorPro\Core\App\Modules\ImportExport\Runners\Revert\Templates as RevertTemplates;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
public function get_name() {
return 'import-export';
}
public function __construct() {
parent::__construct();
$this->add_actions();
}
private function add_actions() {
add_filter( 'elementor/import/get_default_settings_conflicts', function( array $conflicts, array $templates ) {
return $this->apply_conditions_conflicts( $conflicts, $templates );
}, 10, 2 );
add_action( 'elementor/import-export/import-kit', function( Import $import ) {
$this->register_import_kit_runners( $import );
} );
add_action( 'elementor/import-export/export-kit', function( Export $export ) {
$this->register_export_kit_runners( $export );
} );
add_action( 'elementor/import-export/revert-kit', function( Revert $revert ) {
$this->register_revert_kit_runners( $revert );
} );
}
private function apply_conditions_conflicts( $conflicts, $templates ) {
/** @var ThemeBuilderModule $theme_builder_module */
$theme_builder_module = Plugin::instance()->modules_manager->get_modules( 'theme-builder' );
if ( ! $theme_builder_module ) {
return $conflicts;
}
return $conflicts + $theme_builder_module->get_conditions_conflicts( $templates );
}
private function register_import_kit_runners( Import $import ) {
$import->register( new ImportTemplates() );
}
private function register_export_kit_runners( Export $export ) {
$export->register( new ExportTemplates() );
}
private function register_revert_kit_runners( Revert $revert ) {
$revert->register( new RevertTemplates() );
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace ElementorPro\Core\App\Modules\ImportExport\Runners\Export;
use Elementor\App\Modules\ImportExport\Runners\Export\Export_Runner_Base;
use Elementor\Core\Base\Document;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
class Templates extends Export_Runner_Base {
public static function get_name() : string {
return 'templates';
}
public function should_export( array $data ) {
return (
isset( $data['include'] ) &&
in_array( 'templates', $data['include'], true )
);
}
public function export( array $data ) {
$template_types = array_values( Source_Local::get_template_types() );
$query_args = [
'post_type' => Source_Local::CPT,
'post_status' => 'publish',
'posts_per_page' => -1,
'meta_query' => [
[
'key' => Document::TYPE_META_KEY,
'value' => $template_types,
],
],
];
$templates_query = new \WP_Query( $query_args );
$templates_manifest_data = [];
$files = [];
foreach ( $templates_query->posts as $template_post ) {
$template_id = $template_post->ID;
$template_document = Plugin::$instance->documents->get( $template_id );
$templates_manifest_data[ $template_id ] = $template_document->get_export_summary();
$files[] = [
'path' => 'templates/' . $template_id,
'data' => $template_document->get_export_data(),
];
}
$manifest_data['templates'] = $templates_manifest_data;
return [
'files' => $files,
'manifest' => [
$manifest_data,
],
];
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace ElementorPro\Core\App\Modules\ImportExport\Runners\Import;
use Elementor\App\Modules\ImportExport\Runners\Import\Import_Runner_Base;
use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;
use Elementor\Core\Base\Document;
use Elementor\Plugin;
use ElementorPro\Modules\ThemeBuilder\Classes\Conditions_Manager;
use ElementorPro\Modules\ThemeBuilder\Module as ThemeBuilderModule;
use ElementorPro\Plugin as ProPlugin;
use Elementor\TemplateLibrary\Source_Local;
class Templates extends Import_Runner_Base {
private $import_session_id;
private $templates_conditions = [];
public static function get_name() : string {
return 'templates';
}
public function should_import( array $data ) {
return (
isset( $data['include'] ) &&
in_array( 'templates', $data['include'], true ) &&
! empty( $data['extracted_directory_path'] ) &&
! empty( $data['manifest']['templates'] )
);
}
public function import( array $data, array $imported_data ) {
$this->import_session_id = $data['session_id'];
$path = $data['extracted_directory_path'] . 'templates/';
$templates = $data['manifest']['templates'];
$result['templates'] = [
'succeed' => [],
'failed' => [],
];
foreach ( $templates as $id => $template_settings ) {
try {
$template_data = ImportExportUtils::read_json_file( $path . $id );
$import = $this->import_template( $id, $template_settings, $template_data );
$result['templates']['succeed'][ $id ] = $import;
} catch ( \Exception $error ) {
$result['templates']['failed'][ $id ] = $error->getMessage();
}
}
return $result;
}
private function import_template( $id, array $template_settings, array $template_data ) {
$doc_type = $template_settings['doc_type'];
$new_document = Plugin::$instance->documents->create(
$doc_type,
[
'post_title' => $template_settings['title'],
'post_type' => Source_Local::CPT,
'post_status' => 'publish',
]
);
if ( is_wp_error( $new_document ) ) {
throw new \Exception( $new_document->get_error_message() );
}
$template_data['import_settings'] = $template_settings;
$template_data['id'] = $id;
$this->set_templates_conditions( $template_data );
$new_attachment_callback = function( $attachment_id ) {
$this->set_session_post_meta( $attachment_id, $this->import_session_id );
};
add_filter( 'elementor/template_library/import_images/new_attachment', $new_attachment_callback );
$new_document->import( $template_data );
remove_filter( 'elementor/template_library/import_images/new_attachment', $new_attachment_callback );
$document_id = $new_document->get_main_id();
$this->set_session_post_meta( $document_id, $this->import_session_id );
return $document_id;
}
public function get_import_session_metadata() : array {
return [
'template_conditions' => $this->templates_conditions,
];
}
private function set_templates_conditions( $template_data ) {
$conditions = $template_data['import_settings']['conditions'] ?? [];
if ( empty( $conditions ) ) {
return;
}
$condition = $conditions[0];
$condition = rtrim( implode( '/', $condition ), '/' );
/** @var ThemeBuilderModule $theme_builder_module */
$theme_builder_module = ProPlugin::instance()->modules_manager->get_modules( 'theme-builder' );
$conditions_manager = $theme_builder_module->get_conditions_manager();
$conflicts = $conditions_manager->get_conditions_conflicts_by_location(
$condition,
$template_data['import_settings']['location']
);
foreach ( $conflicts as $template ) {
$template_document = Plugin::$instance->documents->get( $template['template_id'] );
$template_conditions = $theme_builder_module->get_conditions_manager()->get_document_conditions( $template_document );
$this->templates_conditions[ $template['template_id'] ] = $template_conditions;
}
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace ElementorPro\Core\App\Modules\ImportExport\Runners\Revert;
use Elementor\App\Modules\ImportExport\Runners\Revert\Revert_Runner_Base;
use Elementor\Core\Base\Document;
use Elementor\Plugin;
use ElementorPro\Modules\ThemeBuilder\Module as ThemeBuilderModule;
use ElementorPro\Plugin as ProPlugin;
use Elementor\TemplateLibrary\Source_Local;
class Templates extends Revert_Runner_Base {
public static function get_name() : string {
return 'templates';
}
public function should_revert( array $data ) : bool {
return (
isset( $data['runners'] ) &&
array_key_exists( static::get_name(), $data['runners'] )
);
}
public function revert( array $data ) {
$template_types = array_values( Source_Local::get_template_types() );
$query_args = [
'post_type' => Source_Local::CPT,
'post_status' => 'any',
'posts_per_page' => -1,
'meta_query' => [
[
'key' => Document::TYPE_META_KEY,
'value' => $template_types,
],
[
'key' => static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID,
'value' => $data['session_id'],
],
],
];
$templates_query = new \WP_Query( $query_args );
foreach ( $templates_query->posts as $template_post ) {
$template_document = Plugin::$instance->documents->get( $template_post->ID );
$template_document->delete();
}
/** @var ThemeBuilderModule $theme_builder_module */
$theme_builder_module = ProPlugin::instance()->modules_manager->get_modules( 'theme-builder' );
$theme_builder_module->get_conditions_manager()->clear_cache();
$old_conditions = $data['runners']['templates']['template_conditions'] ?? [];
foreach ( $old_conditions as $template_id => $conditions ) {
$theme_builder_module->get_conditions_manager()->save_conditions( $template_id, $conditions );
}
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace ElementorPro\Core\App\Modules\KitLibrary;
use ElementorPro\Plugin;
use ElementorPro\License\API;
use ElementorPro\License\Admin;
use Elementor\Core\Base\Module as BaseModule;
use ElementorPro\Core\Connect\Apps\Activate;
use Elementor\Core\App\Modules\KitLibrary\Connect\Kit_Library;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
/**
* Get name.
*
* @access public
*
* @return string
*/
public function get_name() {
return 'kit-library';
}
private function set_kit_library_settings() {
$common = Plugin::elementor()->common;
$app = Plugin::elementor()->app;
$prev_settings = $app->get_settings( 'kit-library' );
// BC Support.
if ( ! $prev_settings || ! $common ) {
return;
}
/** @var Activate $activate */
$activate = $common->get_component( 'connect' )->get_app( 'activate' );
/** @var Kit_Library $kit_library */
$kit_library = $common->get_component( 'connect' )->get_app( 'kit-library' );
$app->set_settings( 'kit-library', array_merge( $prev_settings, [
'is_pro' => true,
'is_library_connected' => API::is_license_active() && $kit_library && $kit_library->is_connected(),
'library_connect_url' => $activate->get_admin_url( 'authorize', [
'utm_source' => 'kit-library',
'utm_medium' => 'wp-dash',
'utm_campaign' => 'connect-and-activate-license',
'utm_term' => '%%page%%', // Will be replaced in the frontend.
] ),
'access_level' => API::get_library_access_level( 'kit' ),
'access_tier' => API::get_access_tier(),
] ) );
}
/**
* @param array $connect_info
* @param $app
*
* @return array
*/
private function add_license_to_connect_info( array $connect_info, $app ) {
$license_key = Admin::get_license_key();
// In elementor 3.3.0-beta it does not send the $app parameter and it should add the license.
$bc_support = ! $app;
$is_kit_library_request = $app && Kit_Library::class === get_class( $app );
if ( ! empty( $license_key ) && ( $bc_support || $is_kit_library_request ) ) {
$connect_info['license'] = $license_key;
}
return $connect_info;
}
public function __construct() {
add_action( 'elementor/init', function () {
$this->set_kit_library_settings();
}, 13 /** after elementor core */ );
add_filter( 'elementor/connect/additional-connect-info', function ( array $connect_info, $app = null ) {
return $this->add_license_to_connect_info( $connect_info, $app );
}, 10, 2 );
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace ElementorPro\Core\App\Modules\Onboarding;
use Elementor\Core\App\Modules\Onboarding\Module as Core_Onboarding_Module;
use ElementorPro\Plugin;
use Elementor\Core\Base\Module as BaseModule;
use ElementorPro\Core\Connect\Apps\Activate;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
/**
* Get name
*
* @since 3.6.0
* @access public
*
* @return string
*/
public function get_name() {
return 'onboarding';
}
/**
* Set Onboarding Settings
*
* Overrides the Onboarding App's Core settings with updated settings to accommodate for Elementor Pro.
*
* @since 3.6.0
* @access private
*/
private function set_onboarding_settings() {
$common = Plugin::elementor()->common;
$app = Plugin::elementor()->app;
$onboarding_settings = $app->get_settings( 'onboarding' );
// If the installed Elementor Core version does not include the Onboarding module, exit here.
if ( ! $onboarding_settings ) {
return;
}
/** @var Activate $activate */
$activate = $common->get_component( 'connect' )->get_app( 'activate' );
$onboarding_settings['urls']['connect'] = $activate->get_admin_url( 'authorize', [
'utm_source' => 'editor-app',
'utm_campaign' => 'connect-account',
'utm_medium' => 'wp-dash',
'utm_term' => Core_Onboarding_Module::VERSION,
'source' => 'generic',
] );
$onboarding_settings['urls']['signUp'] = $activate->get_admin_url( 'authorize', [
'utm_source' => 'editor-app',
'utm_campaign' => 'connect-account',
'utm_medium' => 'wp-dash',
'utm_term' => Core_Onboarding_Module::VERSION,
'source' => 'generic',
'screen_hint' => 'signup',
] );
$app->set_settings( 'onboarding', $onboarding_settings );
}
public function __construct() {
add_action( 'elementor/init', function () {
$this->set_onboarding_settings();
}, 13 /** after elementor core */ );
}
}

View File

@@ -0,0 +1,15 @@
import './indicator-bullet.scss';
export const Indicator = ( props ) => {
let className = 'eps-indicator-bullet';
if ( props.active ) {
className += ` ${ className }--active`;
}
return <i className={ className } />;
};
Indicator.propTypes = {
active: PropTypes.bool,
};

View File

@@ -0,0 +1,34 @@
$eps-indicator-bullet-border-color: theme-colors(light);
$eps-indicator-bullet-dark-border-color: dark-tints(500);
:root {
--indicator-bullet-border-color: #{$eps-indicator-bullet-border-color};
}
.eps-theme-dark {
--indicator-bullet-border-color: #{$eps-indicator-bullet-dark-border-color};
}
$eps-indicator-bullet-size: spacing(16) * 0.75;
$eps-indicator-bullet-color: tints(300);
$eps-indicator-bullet-color-active: theme-colors(success);
$eps-indicator-bullet-box-shadow: $eps-box-shadow-1;
$eps-indicator-bullet-radius: 100%;
$eps-indicator-bullet-border: 2px solid var(--indicator-bullet-border-color);
$eps-indicator-bullet-spacing: spacing(10);
.eps-indicator-bullet {
display: block;
flex-shrink: 0;
width: $eps-indicator-bullet-size;
height: $eps-indicator-bullet-size;
box-shadow: $eps-indicator-bullet-box-shadow;
background-color: $eps-indicator-bullet-color;
border: $eps-indicator-bullet-border;
border-radius: $eps-indicator-bullet-radius;
margin-inline-end: $eps-indicator-bullet-spacing;
&--active {
background-color: $eps-indicator-bullet-color-active;
}
}

View File

@@ -0,0 +1,37 @@
import './preview-iframe.scss';
export default function PreviewIFrame( props ) {
const ref = React.useRef( null ),
previewBreakpoint = 1200,
[ scale, setScale ] = React.useState( 1 ),
[ height, setHeight ] = React.useState( 0 );
// In order to make sure that the iframe itself show the content in specific viewport,
// and it should fit to the size of the card, there is a use of css props `scale` and `height`,
// and another element that wraps the iframe to be the guidelines of the iframe sizes.
React.useEffect( () => {
const currentScale = ref.current.clientWidth / previewBreakpoint;
setScale( currentScale );
setHeight( ref.current.clientHeight / currentScale );
}, [] );
return (
<div
ref={ ref }
className={ `site-editor__preview-iframe site-editor__preview-iframe--${ props.templateType }` }
>
<iframe
title="preview"
src={ props.src }
className={ `site-editor__preview-iframe__iframe` }
style={ { transform: `scale(${ scale })`, height, width: previewBreakpoint } }
/>
</div>
);
}
PreviewIFrame.propTypes = {
src: PropTypes.string.isRequired,
templateType: PropTypes.string.isRequired,
};

View File

@@ -0,0 +1,17 @@
.site-editor__preview-iframe {
height: 50vh;
position: relative;
&__iframe {
top: 0;
left: 0;
position: absolute;
border: none;
transform-origin: 0 0;
height: 100%;
}
&--header, &--footer{
height: 15vh;
}
}

View File

@@ -0,0 +1,48 @@
export class BaseContext extends React.Component {
constructor( props ) {
super( props );
this.state = {
action: {
current: null,
loading: false,
error: null,
errorMeta: {},
},
updateActionState: this.updateActionState.bind( this ),
resetActionState: this.resetActionState.bind( this ),
};
}
executeAction( name, handler ) {
this.updateActionState( { current: name, loading: true, error: null, errorMeta: {} } );
return handler()
.then( ( response ) => {
this.resetActionState();
return Promise.resolve( response );
} )
.catch( ( error ) => {
this.updateActionState( { current: name, loading: false, error: error.message, errorMeta: error } );
return Promise.reject( error );
} );
}
updateActionState( data ) {
return this.setState( ( prev ) => ( {
action: {
...prev.action,
...data,
},
} ) );
}
resetActionState() {
this.updateActionState( { current: null, loading: false, error: null, errorMeta: {} } );
}
}
export default BaseContext;

View File

@@ -0,0 +1,296 @@
import Condition from './models/condition';
import ConditionsConfig from './services/conditions-config';
import BaseContext from './base-context';
import { TemplatesConditions, TemplatesConditionsConflicts } from '../data/commands';
export const Context = React.createContext();
export class ConditionsProvider extends BaseContext {
static propTypes = {
children: PropTypes.any.isRequired,
currentTemplate: PropTypes.object.isRequired,
onConditionsSaved: PropTypes.func.isRequired,
validateConflicts: PropTypes.bool,
};
static defaultProps = {
validateConflicts: true,
};
static actions = {
FETCH_CONFIG: 'fetch-config',
SAVE: 'save',
CHECK_CONFLICTS: 'check-conflicts',
};
/**
* Holds the conditions config object.
*
* @type {ConditionsConfig}
*/
conditionsConfig = null;
/**
* ConditionsProvider constructor.
*
* @param {any} props
*/
constructor( props ) {
super( props );
this.state = {
...this.state,
conditions: {},
updateConditionItemState: this.updateConditionItemState.bind( this ),
removeConditionItemInState: this.removeConditionItemInState.bind( this ),
createConditionItemInState: this.createConditionItemInState.bind( this ),
findConditionItemInState: this.findConditionItemInState.bind( this ),
saveConditions: this.saveConditions.bind( this ),
};
}
/**
* Fetch the conditions config, then normalize the conditions and then setup titles for
* the subIds.
*/
componentDidMount() {
this.executeAction( ConditionsProvider.actions.FETCH_CONFIG, () => ConditionsConfig.create() )
.then( ( conditionsConfig ) => this.conditionsConfig = conditionsConfig )
.then( this.normalizeConditionsState.bind( this ) )
.then( this.setSubIdTitles.bind( this ) );
}
/**
* Execute a request to save the template conditions.
*
* @return {any} Saved conditions
*/
saveConditions() {
const conditions = Object.values( this.state.conditions )
.map( ( condition ) => condition.forDb() );
return this.executeAction(
ConditionsProvider.actions.SAVE,
() => $e.data.update( TemplatesConditions.signature, { conditions }, { id: this.props.currentTemplate.id } ),
).then( () => {
const contextConditions = Object.values( this.state.conditions )
.map( ( condition ) => condition.forContext() );
this.props.onConditionsSaved( this.props.currentTemplate.id, {
conditions: contextConditions,
instances: this.conditionsConfig.calculateInstances( Object.values( this.state.conditions ) ),
isActive: !! ( Object.keys( this.state.conditions ).length && 'publish' === this.props.currentTemplate.status ),
} );
} );
}
/**
* Check for conflicts in the server and mark the condition if there
* is a conflict.
*
* @param {any} condition
*/
checkConflicts( condition ) {
return this.executeAction(
ConditionsProvider.actions.CHECK_CONFLICTS,
() => $e.data.get( TemplatesConditionsConflicts.signature, {
post_id: this.props.currentTemplate.id,
condition: condition.clone().toString(),
} ),
).then( ( response ) =>
this.updateConditionItemState( condition.id, { conflictErrors: Object.values( response.data ) }, false ),
);
}
/**
* Fetching subId titles.
*
* @param {any} condition
* @return {Promise<unknown>} Titles
*/
fetchSubIdsTitles( condition ) {
return new Promise( ( resolve ) => {
return elementorCommon.ajax.loadObjects( {
action: 'query_control_value_titles',
ids: _.isArray( condition.subId ) ? condition.subId : [ condition.subId ],
data: {
get_titles: condition.subIdAutocomplete,
unique_id: elementorCommon.helpers.getUniqueId(),
},
success( response ) {
resolve( response );
},
} );
} );
}
/**
* Get the conditions from the template and normalize it to data structure
* that the components can work with.
*/
normalizeConditionsState() {
this.updateConditionsState( () => {
return this.props.currentTemplate.conditions.reduce( ( current, condition ) => {
const conditionObj = new Condition( {
...condition,
default: this.props.currentTemplate.defaultCondition,
options: this.conditionsConfig.getOptions(),
subOptions: this.conditionsConfig.getSubOptions( condition.name ),
subIdAutocomplete: this.conditionsConfig.getSubIdAutocomplete( condition.sub ),
supIdOptions: condition.subId ? [ { value: condition.subId, label: condition.subId } ] : [],
} );
return {
...current,
[ conditionObj.id ]: conditionObj,
};
}, {} );
} ).then( () => {
Object.values( this.state.conditions ).forEach( ( condition ) => this.checkConflicts( condition ) );
} );
}
/**
* Set titles to the subIds,
* for the first render of the component.
*/
setSubIdTitles() {
return Object.values( this.state.conditions ).forEach( ( condition ) => {
if ( ! condition.subId ) {
return;
}
return this.fetchSubIdsTitles( condition )
.then( ( response ) =>
this.updateConditionItemState( condition.id, {
subIdOptions: [ {
label: Object.values( response )[ 0 ],
value: condition.subId,
} ],
}, false ),
);
} );
}
/**
* Update state of specific condition item.
*
* @param {any} id
* @param {any} args
* @param {boolean} shouldCheckConflicts
*/
updateConditionItemState( id, args, shouldCheckConflicts = true ) {
if ( args.name ) {
args.subOptions = this.conditionsConfig.getSubOptions( args.name );
}
if ( args.sub || args.name ) {
args.subIdAutocomplete = this.conditionsConfig.getSubIdAutocomplete( args.sub );
// In case that the condition has been changed, it will set the options of the subId
// to empty array to let select2 autocomplete handle the options.
args.subIdOptions = [];
}
this.updateConditionsState( ( prev ) => {
const condition = prev[ id ];
return {
...prev,
[ id ]: condition.clone().set( args ),
};
} ).then( () => {
if ( shouldCheckConflicts ) {
this.checkConflicts( this.findConditionItemInState( id ) );
}
} );
}
/**
* Remove a condition item from the state.
*
* @param {any} id
*/
removeConditionItemInState( id ) {
this.updateConditionsState( ( prev ) => {
const newConditions = { ...prev };
delete newConditions[ id ];
return newConditions;
} );
}
/**
* Add a new condition item into the state.
*
* @param {boolean} shouldCheckConflicts
*/
createConditionItemInState( shouldCheckConflicts = true ) {
const defaultCondition = this.props.currentTemplate.defaultCondition,
newCondition = new Condition( {
name: defaultCondition,
default: defaultCondition,
options: this.conditionsConfig.getOptions(),
subOptions: this.conditionsConfig.getSubOptions( defaultCondition ),
subIdAutocomplete: this.conditionsConfig.getSubIdAutocomplete( '' ),
} );
this.updateConditionsState( ( prev ) => ( { ...prev, [ newCondition.id ]: newCondition } ) )
.then( () => {
if ( shouldCheckConflicts ) {
this.checkConflicts( newCondition );
}
} );
}
/**
* Find a condition item from the conditions state.
*
* @param {any} id
* @return {Condition|null} Condition
*/
findConditionItemInState( id ) {
return Object.values( this.state.conditions ).find( ( c ) => c.id === id );
}
/**
* Update the whole conditions state.
*
* @param {Function} callback
* @return {Promise<undefined>} Conditions state
*/
updateConditionsState( callback ) {
return new Promise( ( resolve ) =>
this.setState( ( prev ) => ( { conditions: callback( prev.conditions ) } ), resolve ),
);
}
/**
* Renders the provider.
*
* @return {any} Element
*/
render() {
if ( this.state.action.current === ConditionsProvider.actions.FETCH_CONFIG ) {
if ( this.state.error ) {
return <h3>{ __( 'Error:', 'elementor-pro' ) } { this.state.error }</h3>;
}
if ( this.state.loading ) {
return <h3>{ __( 'Loading', 'elementor-pro' ) }...</h3>;
}
}
return (
<Context.Provider value={ this.state }>
{ this.props.children }
</Context.Provider>
);
}
}
export default ConditionsProvider;

View File

@@ -0,0 +1,72 @@
export default class Condition {
id = elementorCommon.helpers.getUniqueId();
default = '';
type = 'include';
name = '';
sub = '';
subId = '';
options = [];
subOptions = [];
subIdAutocomplete = [];
subIdOptions = [];
conflictErrors = [];
constructor( args ) {
this.set( args );
}
set( args ) {
Object.assign( this, args );
return this;
}
clone() {
return Object.assign( new Condition(), this );
}
remove( keys ) {
if ( ! Array.isArray( keys ) ) {
keys = [ keys ];
}
keys.forEach( ( key ) => {
delete this[ key ];
} );
return this;
}
only( keys ) {
if ( ! Array.isArray( keys ) ) {
keys = [ keys ];
}
const keysToRemove = Object.keys( this )
.filter( ( conditionKey ) => ! keys.includes( conditionKey ) );
this.remove( keysToRemove );
return this;
}
toJson() {
return JSON.stringify( this );
}
toString() {
return this.forDb().filter( ( item ) => item ).join( '/' );
}
forDb() {
return [ this.type, this.name, this.sub, this.subId ];
}
forContext() {
return {
type: this.type,
name: this.name,
sub: this.sub,
subId: this.subId,
};
}
}

View File

@@ -0,0 +1,130 @@
import { ConditionsConfig as ConditionsConfigCommand } from '../../data/commands';
export class ConditionsConfig {
static instance;
config = null;
constructor( config ) {
this.config = config;
}
/**
* @return {Promise<ConditionsConfig>} Conditions config
*/
static create() {
if ( ConditionsConfig.instance ) {
return Promise.resolve( ConditionsConfig.instance );
}
return $e.data.get( ConditionsConfigCommand.signature, {}, { refresh: true } )
.then( ( response ) => {
ConditionsConfig.instance = new ConditionsConfig( response.data );
return ConditionsConfig.instance;
} );
}
/**
* Get main options for condition name.
*
* @return {Array} Condition options
*/
getOptions() {
return this.getSubOptions( 'general', true )
.map( ( { label, value } ) => {
return {
label,
value,
};
} );
}
/**
* Get the sub options for the select.
*
* @param {string} itemName
* @param {boolean} isSubItem
* @return {Array} Sub options
*/
getSubOptions( itemName, isSubItem = false ) {
const config = this.config[ itemName ];
if ( ! config ) {
return [];
}
return [
{ label: config.all_label, value: isSubItem ? itemName : '' },
...config.sub_conditions.map( ( subName ) => {
const subConfig = this.config[ subName ];
return {
label: subConfig.label,
value: subName,
children: subConfig.sub_conditions.length ? this.getSubOptions( subName, true ) : null,
};
} ),
];
}
/**
* Get the autocomplete property from the conditions config
*
* @param {string} sub
* @return {{}|any} Conditions autocomplete
*/
getSubIdAutocomplete( sub ) {
const config = this.config[ sub ];
if ( ! config || ! ( 'object' === typeof ( config.controls ) ) ) {
return {};
}
const controls = Object.values( config.controls );
if ( ! controls?.[ 0 ]?.autocomplete ) {
return {};
}
return controls[ 0 ].autocomplete;
}
/**
* Calculate instances from the conditions.
*
* @param {Array} conditions
* @return {Object} Conditions Instances
*/
calculateInstances( conditions ) {
let instances = conditions.reduce( ( current, condition ) => {
if ( 'exclude' === condition.type ) {
return current;
}
const key = condition.sub || condition.name,
config = this.config[ key ];
if ( ! config ) {
return current;
}
const instanceLabel = condition.subId
? `${ config.label } #${ condition.subId }`
: config.all_label;
return {
...current,
[ key ]: instanceLabel,
};
}, {} );
if ( 0 === Object.keys( instances ).length ) {
instances = [ __( 'No instances', 'elementor-pro' ) ];
}
return instances;
}
}
export default ConditionsConfig;

View File

@@ -0,0 +1,152 @@
import BaseContext from './base-context';
import { Templates } from '../data/commands';
import Component from '../data/component';
export const Context = React.createContext();
export class TemplatesProvider extends BaseContext {
static propTypes = {
children: PropTypes.object.isRequired,
};
static actions = {
FETCH: 'fetch',
DELETE: 'delete',
UPDATE: 'update',
IMPORT: 'import',
};
constructor( props ) {
super( props );
this.state = {
...this.state,
action: {
...this.state.action,
current: TemplatesProvider.actions.FETCH,
loading: true,
},
templates: {},
updateTemplateItemState: this.updateTemplateItemState.bind( this ),
findTemplateItemInState: this.findTemplateItemInState.bind( this ),
fetchTemplates: this.fetchTemplates.bind( this ),
deleteTemplate: this.deleteTemplate.bind( this ),
updateTemplate: this.updateTemplate.bind( this ),
importTemplates: this.importTemplates.bind( this ),
};
}
componentDidMount() {
this.fetchTemplates();
}
importTemplates( { fileName, fileData } ) {
return this.executeAction(
TemplatesProvider.actions.IMPORT,
() => $e.data.create( Templates.signature, { fileName, fileData } ),
).then( ( response ) => {
this.updateTemplatesState( ( prev ) => (
{
...prev,
...Object.values( response.data ).reduce(
( current, template ) => {
if ( ! template.supportsSiteEditor ) {
return current;
}
return { ...current, [ template.id ]: template };
}, {},
),
}
) );
return response;
} );
}
deleteTemplate( id ) {
return this.executeAction(
TemplatesProvider.actions.DELETE,
() => $e.data.delete( Templates.signature, { id } ),
).then( () => {
this.updateTemplatesState( ( prev ) => {
const newTemplates = { ...prev };
delete newTemplates[ id ];
return newTemplates;
} );
} );
}
updateTemplate( id, args ) {
return this.executeAction(
TemplatesProvider.actions.UPDATE,
() => $e.data.update( Templates.signature, args, { id } ),
).then( ( response ) => {
this.updateTemplateItemState( id, response.data );
} );
}
fetchTemplates() {
return this.executeAction(
TemplatesProvider.actions.FETCH,
() => $e.data.get( Templates.signature, {}, { refresh: true } ),
).then( ( response ) => {
this.updateTemplatesState( () => Object.values( response.data ).reduce(
( current, template ) => ( { ...current, [ template.id ]: template } ), {},
), false );
} );
}
updateTemplateItemState( id, args ) {
return this.updateTemplatesState( ( prev ) => {
const template = {
...prev[ id ],
...args,
};
return {
...prev,
[ id ]: template,
};
} );
}
updateTemplatesState( callback, clearCache = true ) {
if ( clearCache ) {
$e.data.deleteCache( $e.components.get( Component.namespace ), Templates.signature );
}
return this.setState( ( prev ) => {
return { templates: callback( prev.templates ) };
} );
}
findTemplateItemInState( id ) {
return this.state.templates[ id ];
}
render() {
if ( this.state.action.current === TemplatesProvider.actions.FETCH ) {
if ( this.state.action.error ) {
return <h3>{ __( 'Error:', 'elementor-pro' ) } { this.state.action.error }</h3>;
}
if ( this.state.action.loading ) {
return <h3>{ __( 'Loading', 'elementor-pro' ) }...</h3>;
}
}
return (
<Context.Provider value={ this.state }>
{ this.props.children }
</Context.Provider>
);
}
}
export default TemplatesProvider;

View File

@@ -0,0 +1,9 @@
export class ConditionsConfig extends $e.modules.CommandData {
static signature = 'site-editor/conditions-config';
static getEndpointFormat() {
return 'site-editor/conditions-config/{id}';
}
}
export default ConditionsConfig;

View File

@@ -0,0 +1,4 @@
export { Templates } from './templates';
export { ConditionsConfig } from './conditions-config';
export { TemplatesConditions } from './templates-conditions';
export { TemplatesConditionsConflicts } from './templates-conditions-conflicts';

View File

@@ -0,0 +1,9 @@
export class TemplatesConditionsConflicts extends $e.modules.CommandData {
static signature = 'site-editor/templates-conditions-conflicts';
static getEndpointFormat() {
return `${ TemplatesConditionsConflicts.signature }/{id}`;
}
}
export default TemplatesConditionsConflicts;

View File

@@ -0,0 +1,9 @@
export class TemplatesConditions extends $e.modules.CommandData {
static signature = 'site-editor/templates-conditions';
static getEndpointFormat() {
return 'site-editor/templates-conditions/{id}';
}
}
export default TemplatesConditions;

View File

@@ -0,0 +1,9 @@
export class Templates extends $e.modules.CommandData {
static signature = 'site-editor/templates';
static getEndpointFormat() {
return 'site-editor/templates/{id}';
}
}
export default Templates;

View File

@@ -0,0 +1,13 @@
import * as dataCommands from './commands';
export default class Component extends $e.modules.ComponentBase {
static namespace = 'site-editor';
getNamespace() {
return this.constructor.namespace;
}
defaultData() {
return this.importCommands( dataCommands );
}
}

View File

@@ -0,0 +1,14 @@
import Component from './data/component';
import { Templates } from './data/commands';
export default class Module extends elementorModules.editor.utils.Module {
onElementorInit() {
const config = elementor.documents.getCurrent().config;
if ( config.support_site_editor ) {
$e.components.register( new Component() );
$e.data.deleteCache( $e.components.get( Component.namespace ), Templates.signature );
}
}
}

View File

@@ -0,0 +1,54 @@
import { Context as TemplatesContext } from '../context/templates';
import useScreenshot, { SCREENSHOT_STATUS_SUCCEED, SCREENSHOT_STATUS_FAILED } from 'modules/screenshots/app/assets/js/hooks/use-screenshot';
/**
* Wrapper function that was made to take screenshots specific for template.
* it will capture a screenshot and update the templates context with the new screenshot.
*
* @param {any} templateType
*/
export default function useTemplatesScreenshot( templateType = null ) {
const { updateTemplateItemState, templates } = React.useContext( TemplatesContext );
const templatesForScreenshot = Object.values( templates ).filter(
( template ) => shouldScreenshotTemplate( template, templateType ),
);
// Start to capture screenshots.
const screenshot = useScreenshot( templatesForScreenshot );
// Update the thumbnail url when screenshot created.
React.useEffect( () => {
screenshot.posts
.filter( ( post ) => post.status === SCREENSHOT_STATUS_SUCCEED )
.forEach( ( post ) => updateTemplateItemState( post.id, { thumbnail: post.imageUrl } ) );
}, [ screenshot.succeed ] );
// Update the screenshot url that was failed.
// When the user will hit the route on the second time it will avoid trying to take another screenshot.
React.useEffect( () => {
screenshot.posts
.filter( ( post ) => post.status === SCREENSHOT_STATUS_FAILED )
.forEach( ( post ) => updateTemplateItemState( post.id, { screenshot_url: null } ) );
}, [ screenshot.failed ] );
return screenshot;
}
/**
* Filter handler.
* will remove all the drafts and private and also will filter by template type if exists.
*
* @param {any} template
* @param {any} templateType
* @return {boolean} should screenshot template
*/
function shouldScreenshotTemplate( template, templateType = null ) {
if ( templateType ) {
return false;
}
return 'publish' === template.status &&
! template.thumbnail &&
template.screenshot_url;
}

View File

@@ -0,0 +1,24 @@
import { Button } from '@elementor/app-ui';
import './back-button.scss';
export default function BackButton( props ) {
return (
<div className="back-button-wrapper">
<Button
className="eps-back-button"
text={ __( 'Back', 'elementor-pro' ) }
icon="eicon-chevron-left"
onClick={ props.onClick }
/>
</div>
);
}
BackButton.propTypes = {
onClick: PropTypes.func,
};
BackButton.defaultProps = {
onClick: () => history.back(),
};

View File

@@ -0,0 +1,8 @@
.#{$eps-prefix}back-button {
font-size: 14px;
margin-block-end: spacing(24);
.#{$eps-prefix}icon {
transform: getValueByDirection(rotate(0deg), rotate(180deg));
}
}

View File

@@ -0,0 +1,33 @@
import { CardBody } from '@elementor/app-ui';
import SiteTemplateThumbnail from './site-template-thumbnail';
import PreviewIFrame from '../atoms/preview-iframe';
export const SiteTemplateBody = ( props ) => {
return (
<CardBody>
{
props.extended
? <PreviewIFrame src={ props.previewUrl } templateType={ props.type } />
: (
<SiteTemplateThumbnail
id={ props.id }
title={ props.title }
type={ props.type }
thumbnail={ props.thumbnail }
placeholder={ props.placeholderUrl }
/>
)
}
</CardBody>
);
};
SiteTemplateBody.propTypes = {
extended: PropTypes.bool,
id: PropTypes.number,
title: PropTypes.string,
thumbnail: PropTypes.string,
placeholderUrl: PropTypes.string,
type: PropTypes.string,
previewUrl: PropTypes.string,
};

View File

@@ -0,0 +1,23 @@
import { Button, CardFooter, Icon, Text } from '@elementor/app-ui';
export const SiteTemplateFooter = ( props ) => {
const instances = Object.values( props.instances ).join( ', ' );
return (
<CardFooter>
<div className="e-site-template__instances">
<Icon className="eicon-flow" />
<Text tag="span" variant="sm"><b>{ __( 'Instances', 'elementor-pro' ) }:</b></Text>
<Text className="e-site-template__instances-list" tag="span" variant="xxs"> { instances }</Text>
<Button text={ __( 'Edit Conditions', 'elementor-pro' ) }
className="e-site-template__edit-conditions"
url={ `/site-editor/conditions/${ props.id }` } />
</div>
</CardFooter>
);
};
SiteTemplateFooter.propTypes = {
id: PropTypes.number.isRequired,
instances: PropTypes.any,
};

View File

@@ -0,0 +1,47 @@
import { Button, CardHeader, Heading, Icon, Text } from '@elementor/app-ui';
import PartActionsButtons from '../part-actions/dialogs-and-buttons';
import { Indicator } from '../atoms/indicator-bullet';
export const SiteTemplateHeader = ( props ) => {
const status = props.status && 'publish' !== props.status ? ` (${ props.status })` : '',
title = props.title + status,
ActionButtons = () => (
<>
<Button text={ __( 'Edit', 'elementor-pro' ) } icon="eicon-edit" className="e-site-template__edit-btn" size="sm" url={ props.editURL } />
<PartActionsButtons { ... props } />
</>
),
MetaDataIcon = ( innerProps ) => (
<Text tag="span" className="e-site-template__meta-data">
<Icon className={ innerProps.icon } />
{ innerProps.content }
</Text>
),
MetaData = () => (
<>
<MetaDataIcon icon="eicon-user-circle-o" content={ props.author } />
<MetaDataIcon icon="eicon-clock-o" content={ props.modifiedDate } />
</>
),
IndicatorDot = props.showInstances ? <Indicator active={ props.isActive } /> : '';
return (
<CardHeader>
{ IndicatorDot }
<Heading tag="h1" title={ title } variant="text-sm" className="eps-card__headline">{ title }</Heading>
{ props.extended && <MetaData /> }
{ props.extended && <ActionButtons /> }
</CardHeader>
);
};
SiteTemplateHeader.propTypes = {
isActive: PropTypes.bool,
author: PropTypes.string,
editURL: PropTypes.string,
extended: PropTypes.bool,
modifiedDate: PropTypes.string,
status: PropTypes.string,
title: PropTypes.string,
showInstances: PropTypes.bool,
};

View File

@@ -0,0 +1,28 @@
import { Button, CardImage, CardOverlay } from '@elementor/app-ui';
export default function SiteTemplateThumbnail( props ) {
return (
<CardImage
alt={ props.title }
src={ props.thumbnail || props.placeholder }
className={ ! props.thumbnail ? 'e-site-template__placeholder' : '' }
>
<CardOverlay className="e-site-template__overlay-preview">
<Button
className="e-site-template__overlay-preview-button"
text={ __( 'Preview', 'elementor-pro' ) }
icon="eicon-preview-medium"
url={ `/site-editor/templates/${ props.type }/${ props.id }` }
/>
</CardOverlay>
</CardImage>
);
}
SiteTemplateThumbnail.propTypes = {
id: PropTypes.number,
title: PropTypes.string,
type: PropTypes.string,
thumbnail: PropTypes.string,
placeholder: PropTypes.string,
};

View File

@@ -0,0 +1,59 @@
import { Card } from '@elementor/app-ui';
import { SiteTemplateHeader } from './site-template-header';
import { SiteTemplateBody } from './site-template-body';
import { SiteTemplateFooter } from './site-template-footer';
import './site-template.scss';
export default function SiteTemplate( props ) {
const baseClassName = 'e-site-template',
classes = [ baseClassName ],
ref = React.useRef( null );
React.useEffect( () => {
if ( ! props.isSelected ) {
return;
}
ref.current.scrollIntoView( {
behavior: 'smooth',
block: 'start',
} );
}, [ props.isSelected ] );
if ( props.extended ) {
classes.push( `${ baseClassName }--extended` );
}
if ( props.aspectRatio ) {
classes.push( `${ baseClassName }--${ props.aspectRatio }` );
}
const CardFooter = props.extended && props.showInstances ? <SiteTemplateFooter { ...props } /> : '';
return (
<Card className={ classes.join( ' ' ) } ref={ ref }>
<SiteTemplateHeader { ... props } />
<SiteTemplateBody { ... props } />
{ CardFooter }
</Card>
);
}
SiteTemplate.propTypes = {
aspectRatio: PropTypes.string,
className: PropTypes.string,
extended: PropTypes.bool,
id: PropTypes.number.isRequired,
isActive: PropTypes.bool.isRequired,
status: PropTypes.string,
thumbnail: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
isSelected: PropTypes.bool,
type: PropTypes.string.isRequired,
showInstances: PropTypes.bool,
};
SiteTemplate.defaultProps = {
isSelected: false,
};

View File

@@ -0,0 +1,117 @@
$eps-meta-icon-color: tints(400);
$eps-meta-icon-dark-color: dark-tints(200);
$preview-button-height: spacing(30);
$image-aspect-ratio: var(--card-image-aspect-ratio, #{$ratio-portrait});
$overlay-preview-padding-block-start: calc(#{$image-aspect-ratio} - #{$preview-button-height});
$aspect-ratio-wide: calc(100% * 0.1235);
:root {
--eps-meta-icon-color: #{$eps-meta-icon-color};
}
.eps-theme-dark {
--eps-meta-icon-color: #{$eps-meta-icon-dark-color};
}
.e-site-template {
&__meta-data {
margin-inline-start: spacing(10);
@include text-truncate();
font-size: type(text, xxs);
&:last-of-type {
margin-inline-end: auto;
}
&:first-of-type {
margin-inline-start: spacing(16);
}
.#{$eps-prefix}icon {
margin-inline-end: spacing(5);
color: var(--eps-meta-icon-color);
font-size: type(text, sm);
}
}
&__placeholder {
.#{$eps-prefix}card__image {
filter: var(--placeholder-filter, none);
}
}
&__overlay-preview {
padding-block-start: $overlay-preview-padding-block-start;
position: relative;
&-button {
font-weight: bold;
display: flex;
flex-wrap: wrap;
height: $preview-button-height;
width: 100%;
background-color: var(--card-background-color-hover);
justify-content: center;
align-items: center;
padding-block-start: spacing(10);
line-height: spacing(20);
--button-background-color: var(--card-headline-color);
&::before {
content: '';
position: absolute;
display: block;
width: 100%;
top: 0;
left: 0;
padding-block-start: $overlay-preview-padding-block-start;
}
& > :not(:first-child) {
margin-inline-start: spacing(5);
}
}
}
&__edit-btn {
margin-inline-end: spacing(20);
.#{$eps-prefix}icon {
margin-inline-end: spacing(5);
}
}
&__instances {
.#{$eps-prefix}icon {
margin-inline-end: spacing(5);
color: var(--eps-meta-icon-color);
font-size: type(text, sm)
}
&-list {
@include text-truncate();
}
}
&__edit-conditions {
margin-inline-start: spacing(16);
text-decoration: underline;
font-style: italic;
}
&--extended {
.#{$eps-prefix}card {
&__figure {
overflow: auto;
}
&__headline {
flex-grow: 0;
}
}
}
&--wide {
--card-image-aspect-ratio: #{$aspect-ratio-wide};
}
}

View File

@@ -0,0 +1,97 @@
import { CssGrid, Dialog } from '@elementor/app-ui';
import SiteTemplate from '../molecules/site-template';
import { PartActionsDialogs } from '../part-actions/dialogs-and-buttons';
import { Context as TemplatesContext } from '../context/templates';
import useTemplatesScreenshot from '../hooks/use-templates-screenshot';
export default function SiteTemplates( props ) {
const { templates: contextTemplates, action, resetActionState } = React.useContext( TemplatesContext );
let gridColumns, templates;
// Make the templates object a memorize value, will re run again only if
// templates has been changed, also sort the templates by `isActive`.
templates = React.useMemo( () => {
return Object.values( contextTemplates )
.sort( ( a, b ) => {
// This sort make sure to show first the active templates, second the
// inactive templates that are not draft, and then the drafts,
// in each category it sorts it inside by date.
if ( ! b.isActive && ! a.isActive ) {
if (
( 'draft' === b.status && 'draft' === a.status ) ||
( 'draft' !== b.status && 'draft' !== a.status )
) {
return b.date < a.date ? 1 : -1;
}
return 'draft' === a.status ? 1 : -1;
}
if ( b.isActive && a.isActive ) {
return b.date < a.date ? 1 : -1;
}
return b.isActive ? 1 : -1;
} );
}, [ contextTemplates ] );
// Start to capture screenshots.
useTemplatesScreenshot( props.type );
const siteTemplateConfig = {};
if ( props.type ) {
templates = templates.filter( ( item ) => item.type === props.type );
siteTemplateConfig.extended = true;
siteTemplateConfig.type = props.type;
switch ( props.type ) {
case 'header':
case 'footer':
gridColumns = 1;
siteTemplateConfig.aspectRatio = 'wide';
break;
default:
gridColumns = 2;
}
}
if ( ! templates || ! templates.length ) {
return <h3>{ __( 'No Templates found. Want to create one?', 'elementor-pro' ) }...</h3>;
}
return (
<section className="e-site-editor__site-templates">
<PartActionsDialogs />
{
action.error &&
<Dialog
text={ action.error }
dismissButtonText={ __( 'Go Back', 'elementor-pro' ) }
dismissButtonOnClick={ resetActionState }
approveButtonText={ __( 'Learn More', 'elementor-pro' ) }
approveButtonColor="link"
approveButtonUrl="https://go.elementor.com/app-theme-builder-template-load-issue"
approveButtonTarget="_target"
/>
}
<CssGrid columns={ gridColumns } spacing={ 24 } colMinWidth={ 200 }>
{
templates.map( ( item ) =>
<SiteTemplate
key={ item.id }
{ ... item }
{ ... siteTemplateConfig }
isSelected={ parseInt( props.id ) === item.id } />,
)
}
</CssGrid>
</section>
);
}
SiteTemplates.propTypes = {
type: PropTypes.string,
id: PropTypes.string,
};

View File

@@ -0,0 +1,53 @@
import { AddNewButton, Heading, Grid, CardOverlay } from '@elementor/app-ui';
import { SiteParts } from '@elementor/site-editor';
import './add-new.scss';
import { Context as TemplatesContext } from '../context/templates';
import BackButton from '../molecules/back-button';
import useFeatureLock from 'elementor-pro-app/hooks/use-feature-lock';
export default function AddNew() {
const { templates } = React.useContext( TemplatesContext ),
hasTemplates = 1 <= Object.keys( templates ).length;
const { isLocked, ConnectButton } = useFeatureLock( 'site-editor' );
/**
* An hover element for each site part.
*
* @param {any} props
*/
const HoverElement = ( props ) => {
if ( isLocked ) {
return (
<CardOverlay className="e-site-editor__promotion-overlay">
<div className="e-site-editor__promotion-overlay__link">
<i className="e-site-editor__promotion-overlay__icon eicon-lock" />
</div>
</CardOverlay>
);
}
return (
<a href={ props.urls.create } className="eps-card__image-overlay eps-add-new__overlay">
<AddNewButton hideText={ true } />
</a>
);
};
HoverElement.propTypes = {
urls: PropTypes.object.isRequired,
};
return (
<section className="e-site-editor__add-new">
<Grid container direction="column" className="e-site-editor__header">
{ hasTemplates && <Grid item><BackButton /></Grid> }
<Grid item container justify="space-between" alignItems="start">
<Heading variant="h1">{ __( 'Start customizing every part of your site', 'elementor-pro' ) }</Heading>
{ isLocked && <ConnectButton /> }
</Grid>
</Grid>
<SiteParts hoverElement={ HoverElement } />
</section>
);
}

View File

@@ -0,0 +1,7 @@
.eps-add-new__overlay {
display: flex;
align-items: center;
justify-content: center;
opacity: 1;
--card-image-overlay-background-color: transparent;
}

View File

@@ -0,0 +1,28 @@
import { Button, Text } from '@elementor/app-ui';
export default function ConditionConflicts( props ) {
if ( ! props.conflicts.length ) {
return '';
}
const conflictLinks = props.conflicts.map( ( conflict ) => {
return (
<Button
key={ conflict.template_id }
target="_blank"
url={ conflict.edit_url }
text={ conflict.template_title }
/>
);
} );
return (
<Text className="e-site-editor-conditions__conflict" variant="sm">
{ __( 'Elementor recognized that you have set this location for other templates: ', 'elementor-pro' ) } { conflictLinks }
</Text>
);
}
ConditionConflicts.propTypes = {
conflicts: PropTypes.array.isRequired,
};

View File

@@ -0,0 +1,28 @@
import { Select } from '@elementor/app-ui';
export default function ConditionName( props ) {
// Hide for template types that has another default, like single & archive.
if ( 'general' !== props.default ) {
return '';
}
const onChange = ( e ) => props.updateConditions( props.id, { name: e.target.value, sub: '', subId: '' } );
return (
<div className="e-site-editor-conditions__input-wrapper">
<Select options={ props.options } value={ props.name } onChange={ onChange } />
</div>
);
}
ConditionName.propTypes = {
updateConditions: PropTypes.func.isRequired,
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
options: PropTypes.array.isRequired,
default: PropTypes.string.isRequired,
};
ConditionName.defaultProps = {
name: '',
};

View File

@@ -0,0 +1,85 @@
import { Select2 } from '@elementor/app-ui';
/**
* Main component.
*
* @param {any} props
* @return {any} Element
* @class
*/
export default function ConditionSubId( props ) {
const settings = React.useMemo( () => (
Object.keys( props.subIdAutocomplete ).length
? getSettings( props.subIdAutocomplete )
: null
), [ props.subIdAutocomplete ] );
if ( ! props.sub || ! settings ) {
return '';
}
const onChange = ( e ) => props.updateConditions( props.id, { subId: e.target.value } );
return (
<div className="e-site-editor-conditions__input-wrapper">
<Select2
onChange={ onChange }
value={ props.subId }
settings={ settings }
options={ props.subIdOptions }
/>
</div>
);
}
/**
* Get settings for the select2 base on the autocomplete settings,
* that passes as a prop
*
* @param {any} autocomplete
* @return {Object} Settings
*/
function getSettings( autocomplete ) {
return {
allowClear: false,
placeholder: __( 'All', 'elementor-pro' ),
dir: elementorCommon.config.isRTL ? 'rtl' : 'ltr',
ajax: {
transport( params, success, failure ) {
return elementorCommon.ajax.addRequest( 'pro_panel_posts_control_filter_autocomplete', {
data: {
q: params.data.q,
autocomplete,
},
success,
error: failure,
} );
},
data( params ) {
return {
q: params.term,
page: params.page,
};
},
cache: true,
},
escapeMarkup( markup ) {
return markup;
},
minimumInputLength: 1,
};
}
ConditionSubId.propTypes = {
subIdAutocomplete: PropTypes.object,
id: PropTypes.string.isRequired,
sub: PropTypes.string,
subId: PropTypes.string,
updateConditions: PropTypes.func,
subIdOptions: PropTypes.array,
};
ConditionSubId.defaultProps = {
subId: '',
subIdOptions: [],
};

View File

@@ -0,0 +1,28 @@
import { Select } from '@elementor/app-ui';
export default function ConditionSub( props ) {
if ( 'general' === props.name || ! props.subOptions.length ) {
return '';
}
const onChange = ( e ) => props.updateConditions( props.id, { sub: e.target.value, subId: '' } );
return (
<div className="e-site-editor-conditions__input-wrapper">
<Select options={ props.subOptions } value={ props.sub } onChange={ onChange } />
</div>
);
}
ConditionSub.propTypes = {
updateConditions: PropTypes.func.isRequired,
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
sub: PropTypes.string.isRequired,
subOptions: PropTypes.array.isRequired,
};
ConditionSub.defaultProps = {
sub: '',
subOptions: {},
};

View File

@@ -0,0 +1,40 @@
import { Select } from '@elementor/app-ui';
export default function ConditionType( props ) {
const wrapperRef = React.createRef();
const options = [
{
label: __( 'Include', 'elementor-pro' ),
value: 'include',
},
{
label: __( 'Exclude', 'elementor-pro' ),
value: 'exclude',
},
];
const onChange = ( e ) => {
props.updateConditions( props.id, { type: e.target.value } );
};
React.useEffect( () => {
wrapperRef.current.setAttribute( 'data-elementor-condition-type', props.type );
} );
return (
<div className="e-site-editor-conditions__input-wrapper e-site-editor-conditions__input-wrapper--condition-type" ref={ wrapperRef }>
<Select options={ options } value={ props.type } onChange={ onChange } />
</div>
);
}
ConditionType.propTypes = {
updateConditions: PropTypes.func.isRequired,
id: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
};
ConditionType.defaultProps = {
type: '',
};

View File

@@ -0,0 +1,95 @@
$e-site-editor-conditions-header-image-button-spacing: spacing(44);
$e-site-editor-conditions-header-image-width: px-to-rem(70);
$e-site-editor-conditions-rows-max-width: px-to-rem(700);
$e-site-editor-conditions-rows-y-spacing: spacing(44);
$e-site-editor-conditions-row-block-start-spacing: spacing(12);
$e-site-editor-conditions-remove-condition-color: tints(400);
$e-site-editor-conditions-remove-condition-font-size: type(size, '18');
$e-site-editor-conditions-row-controls-spacing-end: spacing(10);
$e-site-editor-conditions-row-controls-background: theme-colors(light);
$e-site-editor-conditions-row-controls-dark-background: dark-tints(600);
$e-site-editor-conditions-row-controls-radius: $eps-radius;
$e-site-editor-conditions-row-controls-border: $eps-border-width $eps-border-style tints(100);
$e-site-editor-conditions-row-controls-dark-border: $eps-border-width $eps-border-style tints(700);
//$e-site-editor-conditions-row-controls-dark-border: $eps-border-width $eps-border-style tints(725); //merge after 3.12 is out
$e-site-editor-conditions-row-controls-error-border: $eps-border-width $eps-border-style theme-colors(danger);
$e-site-editor-conditions-conflict-block-start-spacing: spacing(5);
$e-site-editor-conditions-conflict-color: theme-colors(danger);
$e-site-editor-add-button-margin-block-start: spacing(44);
$e-site-editor-add-button-background-color: tints(500);
$e-site-editor-add-button-background-dark-color: dark-tints(500);
$e-site-editor-add-button-color: theme-colors(light);
$e-site-editor-add-button-color-hover-background-color: tints(600);
$e-site-editor-add-button-color-hover-dark-background-color: dark-tints(600);
$e-site-editor-add-button-color-hover-color: theme-colors(light);
$e-site-editor-save-button-container-spacing: spacing(8);
$e-site-editor-input-wrapper-border-width: $eps-border-width;
$e-site-editor-input-wrapper-border-style: $eps-border-style;
$e-site-editor-input-wrapper-border-color: tints(100);
$e-site-editor-input-wrapper-border-dark-color: dark-tints(700);
//$e-site-editor-input-wrapper-border-dark-color: dark-tints(725); //merge after 3.12 is out
$e-site-editor-input-wrapper-select-font-size: type(size, "12");
$e-site-editor-input-wrapper-select-height: px-to-rem(40);
$e-site-editor-input-wrapper-select-y-padding: spacing(10);
$e-site-editor-input-wrapper-select-color: tints(700);
//$e-site-editor-input-wrapper-select-color: tints(725); //merge after 3.12 is out
$e-site-editor-input-wrapper-select-dark-color: dark-tints(200);
$e-site-editor-input-wrapper-select-arrow-font-size: type(size, "12");
$e-site-editor-input-wrapper-select-arrow-margin-end: spacing(10);
$e-site-editor-input-wrapper-condition-type-icon-start-spacing: spacing(12);
$e-site-editor-input-wrapper-condition-type-icon-font-size: type(size, "15");
$e-site-editor-input-wrapper-condition-type-icon-color: theme-colors(light);
$e-site-editor-input-wrapper-condition-type-icon-z-index: z-index(dropdown);
$e-site-editor-input-wrapper-condition-type-arrow-color: theme-colors(light);
$e-site-editor-input-wrapper-condition-type-start-padding: px-to-rem(34);
$e-site-editor-input-wrapper-condition-type-width: px-to-rem(120);
$e-site-editor-input-wrapper-condition-type-font-size: type(size, '12');
$e-site-editor-input-wrapper-condition-type-color: theme-colors(light);
$e-site-editor-input-wrapper-condition-include-background-color: tints(500);
$e-site-editor-input-wrapper-condition-include-background-dark-color: dark-tints(600);
$e-site-editor-input-wrapper-condition-exclude-background-color: tints(400);
$e-site-editor-input-wrapper-condition-exclude-background-dark-color: dark-tints(600);
$e-site-editor-input-select2-search-field-color: theme-elements-colors(text-base-color);
$e-site-editor-input-select2-search-field-dark-color: theme-colors(light);
:root {
--e-site-editor-conditions-row-controls-background: #{$e-site-editor-conditions-row-controls-background};
--e-site-editor-input-wrapper-border-color: #{$e-site-editor-input-wrapper-border-color};
--e-site-editor-input-wrapper-select-color: #{$e-site-editor-input-wrapper-select-color};
--e-site-editor-conditions-row-controls-border: #{$e-site-editor-conditions-row-controls-border};
--e-site-editor-add-button-background-color: #{$e-site-editor-add-button-background-color};
--e-site-editor-add-button-color-hover-background-color: #{$e-site-editor-add-button-color-hover-background-color};
--e-site-editor-input-wrapper-condition-include-background-color:
#{$e-site-editor-input-wrapper-condition-include-background-color};
--e-site-editor-input-wrapper-condition-exclude-background-color:
#{$e-site-editor-input-wrapper-condition-exclude-background-color};
--e-site-editor-input-select2-search-field-color: #{$e-site-editor-input-select2-search-field-color}
}
.eps-theme-dark {
--select2-selection-background-color: tints(600);
--e-site-editor-conditions-row-controls-background: #{$e-site-editor-conditions-row-controls-dark-background};
--e-site-editor-input-wrapper-border-color: #{$e-site-editor-input-wrapper-border-dark-color};
--e-site-editor-input-wrapper-select-color: #{$e-site-editor-input-wrapper-select-dark-color};
--e-site-editor-conditions-row-controls-border: #{$e-site-editor-conditions-row-controls-dark-border};
--e-site-editor-add-button-background-color: #{$e-site-editor-add-button-background-dark-color};
--e-site-editor-add-button-color-hover-background-color: #{$e-site-editor-add-button-color-hover-dark-background-color};
--e-site-editor-input-wrapper-condition-include-background-color:
#{$e-site-editor-input-wrapper-condition-include-background-dark-color};
--e-site-editor-input-wrapper-condition-exclude-background-color:
#{$e-site-editor-input-wrapper-condition-exclude-background-dark-color};
--e-site-editor-input-select2-search-field-color: #{$e-site-editor-input-select2-search-field-dark-color}
}

View File

@@ -0,0 +1,89 @@
import { Context as ConditionsContext, ConditionsProvider } from '../../context/conditions';
import { Button, Dialog } from '@elementor/app-ui';
import ConditionType from './condition-type';
import ConditionName from './condition-name';
import ConditionSub from './condition-sub';
import ConditionSubId from './condition-sub-id';
import ConditionConflicts from './condition-conflicts';
export default function ConditionsRows( props ) {
const {
conditions,
createConditionItemInState: create,
updateConditionItemState: update,
removeConditionItemInState: remove,
saveConditions: save,
action,
resetActionState,
} = React.useContext( ConditionsContext );
const rows = Object.values( conditions ).map( ( condition ) =>
<div key={ condition.id }>
<div className="e-site-editor-conditions__row">
<div
className={ `e-site-editor-conditions__row-controls ${ condition.conflictErrors.length && 'e-site-editor-conditions__row-controls--error' }` }>
<ConditionType { ...condition } updateConditions={ update } />
<div className="e-site-editor-conditions__row-controls-inner">
<ConditionName { ...condition } updateConditions={ update } />
<ConditionSub { ...condition } updateConditions={ update } />
<ConditionSubId { ...condition } updateConditions={ update } />
</div>
</div>
<Button
className="e-site-editor-conditions__remove-condition"
text={ __( 'Delete', 'elementor-pro' ) }
icon="eicon-close"
hideText={ true }
onClick={ () => remove( condition.id ) }
/>
</div>
<ConditionConflicts conflicts={ condition.conflictErrors } />
</div>,
);
const isSaving = action.current === ConditionsProvider.actions.SAVE && action.loading;
return (
<>
{
action.error &&
<Dialog
text={ action.error }
dismissButtonText={ __( 'Go Back', 'elementor-pro' ) }
dismissButtonOnClick={ resetActionState }
approveButtonText={ __( 'Learn More', 'elementor-pro' ) }
approveButtonColor="link"
approveButtonUrl="https://go.elementor.com/app-theme-builder-conditions-load-issue"
approveButtonTarget="_target"
/>
}
<div className="e-site-editor-conditions__rows">
{ rows }
</div>
<div className="e-site-editor-conditions__add-button-container">
<Button
className="e-site-editor-conditions__add-button"
variant="contained"
size="lg"
text={ __( 'Add Condition', 'elementor-pro' ) }
onClick={ create }
/>
</div>
<div className="e-site-editor-conditions__footer">
<Button
variant="contained"
color="primary"
size="lg"
hideText={ isSaving }
icon={ isSaving ? 'eicon-loading eicon-animation-spin' : '' }
text={ __( 'Save & Close', 'elementor-pro' ) }
onClick={ () => save().then( props.onAfterSave ) }
/>
</div>
</>
);
}
ConditionsRows.propTypes = {
onAfterSave: PropTypes.func,
};

View File

@@ -0,0 +1,44 @@
import { Heading, Text } from '@elementor/app-ui';
import ConditionsProvider from '../../context/conditions';
import { Context as TemplatesContext } from '../../context/templates';
import ConditionsRows from './conditions-rows';
import './conditions.scss';
import BackButton from '../../molecules/back-button';
export default function Conditions( props ) {
const { findTemplateItemInState, updateTemplateItemState } = React.useContext( TemplatesContext ),
template = findTemplateItemInState( parseInt( props.id ) );
if ( ! template ) {
return <div>{ __( 'Not Found', 'elementor-pro' ) }</div>;
}
return (
<section className="e-site-editor-conditions">
<BackButton />
<div className="e-site-editor-conditions__header">
<img
className="e-site-editor-conditions__header-image"
src={ `${ elementorAppProConfig.baseUrl }/modules/theme-builder/assets/images/conditions-tab.svg` }
alt={ __( 'Import template', 'elementor-pro' ) }
/>
<Heading variant="h1" tag="h1">
{ __( 'Where Do You Want to Display Your Template?', 'elementor-pro' ) }
</Heading>
<Text variant="p">
{ __( 'Set the conditions that determine where your template is used throughout your site.', 'elementor-pro' ) }
<br />
{ __( 'For example, choose \'Entire Site\' to display the template across your site.', 'elementor-pro' ) }
</Text>
</div>
<ConditionsProvider currentTemplate={ template } onConditionsSaved={ updateTemplateItemState }>
<ConditionsRows onAfterSave={ () => history.back() } />
</ConditionsProvider>
</section>
);
}
Conditions.propTypes = {
id: PropTypes.string,
};

View File

@@ -0,0 +1,187 @@
@import "conditions-api";
.e-site-editor-conditions {
&__header {
text-align: center;
}
&__header-image {
display: block;
margin: 0 auto $e-site-editor-conditions-header-image-button-spacing;
width: $e-site-editor-conditions-header-image-width;
}
&__rows {
margin: $e-site-editor-conditions-rows-y-spacing auto;
max-width: $e-site-editor-conditions-rows-max-width;
}
&__row {
display: flex;
flex-grow: 1;
margin-block-start: $e-site-editor-conditions-row-block-start-spacing;
}
&__remove-condition {
color: $e-site-editor-conditions-remove-condition-color;
font-size: $e-site-editor-conditions-remove-condition-font-size;
display: flex;
align-items: center;
justify-content: center;
}
&__row-controls {
overflow: hidden;
margin-inline-end: $e-site-editor-conditions-row-controls-spacing-end;
background-color: var(--e-site-editor-conditions-row-controls-background);
display: flex;
width: 100%;
border: var(--e-site-editor-conditions-row-controls-border);
border-radius: $e-site-editor-conditions-row-controls-radius;
&--error {
border: $e-site-editor-conditions-row-controls-error-border;
}
}
&__conflict {
text-align: center;
margin-block-start: $e-site-editor-conditions-conflict-block-start-spacing;
color: $e-site-editor-conditions-conflict-color;
}
&__row-controls-inner {
width: 100%;
display: flex;
div {
flex: 1;
}
}
&__add-button-container {
text-align: center;
}
&__add-button {
margin-block-start: $e-site-editor-add-button-margin-block-start;
background-color: var(--e-site-editor-add-button-background-color);
color: $e-site-editor-add-button-color;
text-transform: uppercase;
&:hover {
background-color: var(--e-site-editor-add-button-color-hover-background-color);
color: $e-site-editor-add-button-color-hover-color;
}
}
&__footer {
display: flex;
justify-content: flex-end;
position: absolute;
bottom: 0;
right: 0;
left: 0;
padding: $e-site-editor-save-button-container-spacing;
border-block-start: 1px solid var(--hr-color);
}
&__input-wrapper {
position: relative;
padding-inline-start: $e-site-editor-input-wrapper-border-width $e-site-editor-input-wrapper-border-style;
border-color: var(--e-site-editor-input-wrapper-border-color);
&:first-child {
border: none;
}
select {
appearance: none;
-webkit-appearance: none;
font-size: $e-site-editor-input-wrapper-select-font-size;
height: $e-site-editor-input-wrapper-select-height;
border-width: 0;
padding: 0 $e-site-editor-input-wrapper-select-y-padding;
width: 100%;
position: relative;
color: var(--e-site-editor-input-wrapper-select-color);
outline: none;
background: transparent;
}
&:after {
font-family: eicons;
content: '\e8ad';
font-size: $e-site-editor-input-wrapper-select-arrow-font-size;
pointer-events: none;
position: absolute;
top: 50%;
transform: translateY(-50%);
@include end($e-site-editor-input-wrapper-select-arrow-margin-end);
}
.select2-container--default .select2-selection--single {
border: none;
line-height: $e-site-editor-input-wrapper-select-height;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
line-height: $e-site-editor-input-wrapper-select-height;
font-size: $e-site-editor-input-wrapper-select-font-size;
}
.select2-selection {
outline: none;
background: transparent;
height: $e-site-editor-input-wrapper-select-height;
}
.select2-selection__arrow {
display: none;
}
}
&__input-wrapper--condition-type {
position: relative;
&:before {
font-family: eicons;
position: absolute;
top: 50%;
transform: translateY(-50%);
@include start($e-site-editor-input-wrapper-condition-type-icon-start-spacing);
font-size: $e-site-editor-input-wrapper-condition-type-icon-font-size;
//color: $e-site-editor-input-wrapper-condition-type-icon-color;
pointer-events: none;
z-index: $e-site-editor-input-wrapper-condition-type-icon-z-index;
}
select {
text-transform: uppercase;
padding-inline-start: $e-site-editor-input-wrapper-condition-type-start-padding;
width: $e-site-editor-input-wrapper-condition-type-width;
font-size: $e-site-editor-input-wrapper-condition-type-font-size;
border-inline-end: $e-site-editor-input-wrapper-border-width $e-site-editor-input-wrapper-border-style;
border-color: var(--e-site-editor-input-wrapper-border-color);
}
&[data-elementor-condition-type="include"] {
&:before {
content: '\e8cc';
}
}
&[data-elementor-condition-type="exclude"] {
&:before {
content: '\e8cd';
}
}
}
}
// This is a temporary fix that handles dark mode in select2.
// TODO: Remove it if already handled by the Select2 component.
.select2-search__field {
background-color: transparent;
color: var(--e-site-editor-input-select2-search-field-color);
}

View File

@@ -0,0 +1,119 @@
import { DropZone, Dialog, Checkbox } from '@elementor/app-ui';
import { Context as TemplatesContext, TemplatesProvider } from '../context/templates';
import BackButton from '../molecules/back-button';
import { useConfirmAction as useConfirmActionBase } from '@elementor/hooks';
// The hook `useConfirmAction` comes from the core plugin, so it is possible that it is not available.
const useConfirmActionFallback = ( { action } ) => ( {
runAction: action,
dialog: { isOpen: false },
} );
const useConfirmAction = useConfirmActionBase ?? useConfirmActionFallback;
export default function Import() {
const { importTemplates, action, resetActionState } = React.useContext( TemplatesContext ),
[ importedTemplate, setImportedTemplate ] = React.useState( null ),
isImport = action.current === TemplatesProvider.actions.IMPORT,
isUploading = isImport && action.loading,
hasError = isImport && action.error;
const upload = React.useCallback( ( file ) => {
if ( isUploading ) {
return;
}
readFile( file )
.then( ( fileData ) => importTemplates( { fileName: file.name, fileData } ) )
.then( ( response ) => {
// For now it show a dialog for the first template ONLY!
setImportedTemplate( response.data[ 0 ] );
} );
}, [] );
const {
runAction: uploadFile,
dialog,
checkbox,
} = useConfirmAction( {
doNotShowAgainKey: 'upload_json_warning_generic_message',
action: upload,
} );
return (
<section className="site-editor__import">
{
importedTemplate &&
<Dialog
title={ __( 'Your template was imported', 'elementor-pro' ) }
approveButtonText={ __( 'Preview', 'elementor-pro' ) }
approveButtonUrl={ importedTemplate.url }
approveButtonTarget="_blank"
dismissButtonText={ __( 'Edit', 'elementor-pro' ) }
dismissButtonUrl={ importedTemplate.editURL }
dismissButtonTarget="_top"
onClose={ () => setImportedTemplate( null ) }
/>
}
{
hasError &&
<Dialog
title={ action.error }
approveButtonText={ __( 'Learn More', 'elementor-pro' ) }
approveButtonUrl="https://go.elementor.com/app-theme-builder-import-issue"
approveButtonTarget="_blank"
approveButtonColor="link"
dismissButtonText={ __( 'Go Back', 'elementor-pro' ) }
dismissButtonOnClick={ resetActionState }
onClose={ resetActionState }
/>
}
{
dialog.isOpen &&
<Dialog
title={ __( 'Warning: JSON or ZIP files may be unsafe', 'elementor-pro' ) }
text={ __( 'Uploading JSON or ZIP files from unknown sources can be harmful and put your site at risk. For maximum safety, upload only JSON or ZIP files from trusted sources.', 'elementor-pro' ) }
approveButtonColor="link"
approveButtonText={ __( 'Continue', 'elementor-pro' ) }
approveButtonOnClick={ dialog.approve }
dismissButtonText={ __( 'Cancel', 'elementor-pro' ) }
dismissButtonOnClick={ dialog.dismiss }
onClose={ dialog.dismiss }
>
<label htmlFor="do-not-show-upload-json-warning-again" style={ { display: 'flex', alignItems: 'center', gap: '5px' } }>
<Checkbox
id="do-not-show-upload-json-warning-again"
type="checkbox"
value={ checkbox.isChecked }
onChange={ ( event ) => checkbox.setIsChecked( !! event.target.checked ) }
/>
{ __( 'Do not show this message again', 'elementor-pro' ) }
</label>
</Dialog>
}
<BackButton />
<DropZone
heading={ __( 'Import Template To Your Library', 'elementor-pro' ) }
text={ __( 'Drag & Drop your .JSON or .zip template file', 'elementor-pro' ) }
secondaryText={ __( 'or', 'elementor-pro' ) }
onFileSelect={ uploadFile }
isLoading={ isUploading }
filetypes={ [ 'zip', 'json' ] }
/>
</section>
);
}
function readFile( file ) {
return new Promise( ( ( resolve ) => {
const fileReader = new FileReader();
fileReader.readAsDataURL( file );
fileReader.onload = ( event ) => {
// Replace the mime type that prepended to the base64 with empty string and return a
// resolved promise only with the base64 string.
resolve( event.target.result.replace( /^[^,]+,/, '' ) );
};
} ) );
}

View File

@@ -0,0 +1,37 @@
import { TemplateTypesContext } from '@elementor/site-editor';
import { AddNewButton, Grid, Heading, NotFound } from '@elementor/app-ui';
import SiteTemplates from '../organisms/site-templates';
import useFeatureLock from 'elementor-pro-app/hooks/use-feature-lock';
import './template-type.scss';
export default function TemplateType( props ) {
const { templateTypes } = React.useContext( TemplateTypesContext ),
currentType = templateTypes.find( ( item ) => item.type === props.type ),
{ isLocked, ConnectButton } = useFeatureLock( 'site-editor' );
if ( ! currentType ) {
return <NotFound />;
}
return (
<section className={ `e-site-editor__templates e-site-editor__templates--type-${ props.type }` }>
<Grid className="page-header" container justify="space-between">
<Heading variant="h1">{ currentType.page_title }</Heading>
{
isLocked
? <ConnectButton />
: <AddNewButton url={ currentType.urls.create } text={ __( 'Add New', 'elementor-pro' ) } />
}
</Grid>
<hr className="eps-separator" />
<SiteTemplates type={ currentType.type } id={ props.id } />
</section>
);
}
TemplateType.propTypes = {
type: PropTypes.string,
page_title: PropTypes.string,
id: PropTypes.string,
};

View File

@@ -0,0 +1,13 @@
.e-site-editor__templates {
.page-header {
margin-block-end: spacing(10);
> a {
align-self: baseline;
}
}
.eps-separator {
margin-block-end: spacing(44);
}
}

View File

@@ -0,0 +1,22 @@
import SiteTemplates from '../organisms/site-templates';
import { AddNewButton, Grid } from '@elementor/app-ui';
import useFeatureLock from 'elementor-pro-app/hooks/use-feature-lock';
export default function Templates() {
const { isLocked, ConnectButton } = useFeatureLock( 'site-editor' );
return (
<section className="e-site-editor__site-templates">
<Grid container justify="space-between" alignItems="start" className="page-header">
<h1>{ __( 'Your Site\'s Global Parts', 'elementor-pro' ) }</h1>
{
isLocked
? <ConnectButton />
: <AddNewButton url="/site-editor/add-new" />
}
</Grid>
<hr className="eps-separator" />
<SiteTemplates />
</section>
);
}

View File

@@ -0,0 +1,38 @@
import { Dialog } from '@elementor/app-ui';
import { Context as TemplatesContext } from '../context/templates';
export default function DialogDelete( props ) {
const { deleteTemplate, findTemplateItemInState } = React.useContext( TemplatesContext );
const closeDialog = ( shouldUpdate ) => {
props.setId( null );
if ( shouldUpdate ) {
deleteTemplate( props.id );
}
};
if ( ! props.id ) {
return '';
}
const template = findTemplateItemInState( props.id );
return (
<Dialog
title={ __( 'Move Item To Trash', 'elementor-pro' ) }
text={ __( 'Are you sure you want to move this item to trash:', 'elementor-pro' ) + ` "${ template.title }"` }
onSubmit={ () => closeDialog( true ) }
approveButtonText={ __( 'Move to Trash', 'elementor-pro' ) }
approveButtonOnClick={ () => closeDialog( true ) }
approveButtonColor="danger"
dismissButtonText={ __( 'Cancel', 'elementor-pro' ) }
dismissButtonOnClick={ () => closeDialog() }
onClose={ () => closeDialog() }
/>
);
}
DialogDelete.propTypes = {
id: PropTypes.number,
setId: PropTypes.func.isRequired,
};

View File

@@ -0,0 +1,56 @@
import { useEffect } from 'react';
import { Dialog } from '@elementor/app-ui';
import { Context as TemplatesContext } from '../context/templates';
export default function DialogRename( props ) {
const { findTemplateItemInState, updateTemplate } = React.useContext( TemplatesContext ),
template = findTemplateItemInState( props.id );
const [ title, setTitle ] = React.useState( '' );
useEffect( () => {
// The "title" state should be updated if the template title changed.
if ( template ) {
setTitle( template.title );
}
}, [ template ] );
const closeDialog = ( shouldUpdate ) => {
props.setId( null );
if ( shouldUpdate ) {
updateTemplate( props.id, { post_title: title } );
}
};
if ( ! props.id ) {
return '';
}
return (
<Dialog
title={ __( 'Rename Site Part', 'elementor-pro' ) }
approveButtonText={ __( 'Change', 'elementor-pro' ) }
onSubmit={ () => closeDialog( true ) }
approveButtonOnClick={ () => closeDialog( true ) }
approveButtonColor="primary"
dismissButtonText={ __( 'Cancel', 'elementor-pro' ) }
dismissButtonOnClick={ () => closeDialog() }
onClose={ () => closeDialog() }
>
<input
type="text"
className="eps-input eps-input-text eps-input--block"
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
value={ title }
onChange={ ( e ) => setTitle( e.target.value ) }
/>
</Dialog>
);
}
DialogRename.propTypes = {
id: PropTypes.number,
setId: PropTypes.func.isRequired,
};

View File

@@ -0,0 +1,79 @@
import DialogRename from './dialog-rename';
import DialogDelete from './dialog-delete';
import { Button, Popover } from '@elementor/app-ui';
export const handlers = {
rename: null,
delete: null,
};
// TODO: Think about refactor to portals: https://reactjs.org/docs/portals.html
export function PartActionsDialogs() {
const [ DialogRenameId, setDialogRenameId ] = React.useState( null );
const [ DialogDeleteId, setDialogDeleteId ] = React.useState( null );
handlers.rename = setDialogRenameId;
handlers.delete = setDialogDeleteId;
return (
<>
<DialogRename id={ DialogRenameId } setId={ setDialogRenameId } />
<DialogDelete id={ DialogDeleteId } setId={ setDialogDeleteId } />
</>
);
}
export default function PartActionsButtons( props ) {
const [ showMenu, setShowMenu ] = React.useState( false );
let SiteTemplatePopover = '';
if ( showMenu ) {
SiteTemplatePopover = (
<Popover closeFunction={ () => setShowMenu( ! showMenu ) }>
<li>
<Button
className="eps-popover__item"
icon="eicon-sign-out"
text={ __( 'Export', 'elementor-pro' ) }
url={ props.exportLink }
/>
</li>
<li>
<Button
className="eps-popover__item eps-popover__item--danger"
icon="eicon-trash-o"
text={ __( 'Trash', 'elementor-pro' ) }
onClick={ () => handlers.delete( props.id ) }
/>
</li>
<li>
<Button
className="eps-popover__item"
icon="eicon-edit"
text={ __( 'Rename', 'elementor-pro' ) }
onClick={ () => handlers.rename( props.id ) }
/>
</li>
</Popover>
);
}
return (
<div className="eps-popover__container">
<Button
text={ __( 'Toggle', 'elementor-pro' ) }
hideText={ true }
icon="eicon-ellipsis-h"
size="lg"
onClick={ () => setShowMenu( ! showMenu ) }
/>
{ SiteTemplatePopover }
</div>
);
}
PartActionsButtons.propTypes = {
id: PropTypes.number.isRequired,
exportLink: PropTypes.string.isRequired,
};

View File

@@ -0,0 +1,90 @@
import { Router, LocationProvider, Redirect } from '@reach/router';
import Templates from './pages/templates';
import TemplateType from './pages/template-type';
import AddNew from './pages/add-new';
import Conditions from './pages/conditions/conditions';
import Import from './pages/import';
import TemplatesProvider, { Context as TemplatesContext } from './context/templates';
import { Layout, AllPartsButton, NotFound } from '@elementor/site-editor';
import { ErrorBoundary, Grid, Button } from '@elementor/app-ui';
import router from '@elementor/router';
import Component from './data/component';
import useFeatureLock from 'elementor-pro-app/hooks/use-feature-lock';
import './site-editor.scss';
function SiteEditor() {
const { isLocked } = useFeatureLock( 'site-editor' );
const basePath = 'site-editor';
const headerButtons = [
{
id: 'import',
text: __( 'import', 'elementor-pro' ),
hideText: true,
icon: 'eicon-upload-circle-o',
onClick: () => router.appHistory.navigate( basePath + '/import' ),
},
];
// Remove Core cache.
elementorCommon.ajax.invalidateCache( {
unique_id: 'app_site_editor_template_types',
} );
const SiteEditorDefault = () => {
const { templates } = React.useContext( TemplatesContext );
if ( Object.keys( templates ).length ) {
return <Redirect from={ '/' } to={ '/' + basePath + '/templates' } noThrow={ true } />;
}
return <Redirect from={ '/' } to={ '/' + basePath + '/add-new' } noThrow={ true } />;
};
return (
<ErrorBoundary
title={ __( 'Theme Builder could not be loaded', 'elementor-pro' ) }
learnMoreUrl="https://go.elementor.com/app-theme-builder-load-issue"
>
<Layout allPartsButton={ <AllPartsButton url={ '/' + basePath } /> } headerButtons={ headerButtons } titleRedirectRoute={ '/' + basePath } promotion={ isLocked }>
<Grid container className="e-site-editor__content_container">
<Grid item className="e-site-editor__content_container_main">
<TemplatesProvider>
<LocationProvider history={ router.appHistory }>
<Router>
<SiteEditorDefault path={ basePath } />
<Templates path={ basePath + '/templates' } />
<TemplateType path={ basePath + '/templates/:type/*id' } />
<AddNew path={ basePath + '/add-new' } />
<Conditions path={ basePath + '/conditions/:id' } />
<Import path={ basePath + '/import' } />
<NotFound default />
</Router>
</LocationProvider>
</TemplatesProvider>
</Grid>
<Grid item className="e-site-editor__content_container_secondary">
<Button
text={ __( 'Switch to table view', 'elementor-pro' ) }
url={ elementorAppProConfig[ 'site-editor' ]?.urls?.legacy_view }
/>
</Grid>
</Grid>
</Layout>
</ErrorBoundary>
);
}
export default class Module {
constructor() {
elementorCommon.debug.addURLToWatch( 'elementor-pro/assets' );
$e.components.register( new Component() );
router.addRoute( {
path: '/site-editor/*',
component: SiteEditor,
} );
}
}

View File

@@ -0,0 +1,14 @@
.e-site-editor__content_container {
flex-direction: column;
min-height: 100%;
flex-wrap: nowrap;
}
.e-site-editor__content_container_main {
flex: 1;
}
.e-site-editor__content_container_secondary {
margin: 0 auto;
padding-block-start: spacing(44);
}

View File

@@ -0,0 +1,26 @@
<?php
namespace ElementorPro\Core\App\Modules\SiteEditor\Data;
use ElementorPro\Plugin;
use Elementor\Data\Base\Controller as Controller_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Controller extends Controller_Base {
public function get_name() {
return 'site-editor';
}
public function register_endpoints() {
$this->register_endpoint( Endpoints\Templates::class );
$this->register_endpoint( Endpoints\Conditions_Config::class );
$this->register_endpoint( Endpoints\Templates_Conditions::class );
$this->register_endpoint( Endpoints\Templates_Conditions_Conflicts::class );
}
public function get_permission_callback( $request ) {
return Plugin::elementor()->kits_manager->get_active_kit()->is_editable_by_current_user();
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace ElementorPro\Core\App\Modules\SiteEditor\Data\Endpoints;
use Elementor\Data\Base\Endpoint;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Base_Endpoint extends Endpoint {
/**
* Check if post is lock.
*
* @param $post_id
*
* @return bool|false|int
*/
protected function is_post_lock( $post_id ) {
require_once ABSPATH . 'wp-admin/includes/post.php';
return wp_check_post_lock( $post_id );
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace ElementorPro\Core\App\Modules\SiteEditor\Data\Endpoints;
use ElementorPro\Modules\ThemeBuilder\Module as ThemeBuilderModule;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Conditions_Config extends Base_Endpoint {
/**
* @return string
*/
public function get_name() {
return 'conditions-config';
}
public function get_items( $request ) {
$conditions_manager = ThemeBuilderModule::instance()->get_conditions_manager();
return $conditions_manager->get_conditions_config();
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace ElementorPro\Core\App\Modules\SiteEditor\Data\Endpoints;
use ElementorPro\Plugin;
use ElementorPro\Core\App\Modules\SiteEditor\Module;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Template_Types extends Base_Endpoint {
/**
* @return string
*/
public function get_name() {
return 'template-types';
}
public function get_items( $request ) {
/** @var Module $site_editor_module */
$site_editor_module = Plugin::instance()->app->get_component( 'site-editor' );
return $site_editor_module->get_template_types();
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace ElementorPro\Core\App\Modules\SiteEditor\Data\Endpoints;
use ElementorPro\Plugin;
use ElementorPro\Modules\ThemeBuilder\Module;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Templates_Conditions_Conflicts extends Base_Endpoint {
/**
* @return string
*/
public function get_name() {
return 'templates-conditions-conflicts';
}
public function get_items( $request ) {
/** @var Module $theme_builder */
$theme_builder = Plugin::instance()->modules_manager->get_modules( 'theme-builder' );
return $theme_builder
->get_conditions_manager()
->get_conditions_conflicts( intval( $request['post_id'] ), $request['condition'] );
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace ElementorPro\Core\App\Modules\SiteEditor\Data\Endpoints;
use ElementorPro\Plugin;
use Elementor\Core\Utils\Exceptions;
use ElementorPro\Modules\ThemeBuilder\Module;
use ElementorPro\Core\App\Modules\SiteEditor\Data\Responses\Lock_Error_Response;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Templates_Conditions extends Base_Endpoint {
/**
* @return string
*/
public function get_name() {
return 'templates-conditions';
}
protected function register() {
$this->register_item_route();
$this->register_item_route( \WP_REST_Server::EDITABLE );
}
public function get_item( $template_id, $request ) {
return $this->get_conditions( $template_id );
}
public function update_item( $template_id, $request ) {
$lock_by_user_id = $this->is_post_lock( $template_id );
if ( $lock_by_user_id ) {
return new Lock_Error_Response( $lock_by_user_id );
}
$data = $request->get_body_params();
if ( ! isset( $data['conditions'] ) ) {
$data['conditions'] = [];
}
$is_saved = $this->save_conditions( $template_id, $data['conditions'] );
if ( ! $is_saved ) {
return new \WP_Error(
'conditions',
__( 'Error while saving conditions.', 'elementor-pro' ),
[ 'status' => Exceptions::INTERNAL_SERVER_ERROR ]
);
}
return true;
}
protected function get_conditions( $post_id ) {
$document = \Elementor\Plugin::$instance->documents->get( $post_id );
/** @var Module $theme_builder */
$theme_builder = Plugin::instance()->modules_manager->get_modules( 'theme-builder' );
return $theme_builder
->get_conditions_manager()
->get_document_conditions( $document );
}
protected function save_conditions( $post_id, $conditions ) {
/** @var Module $theme_builder */
$theme_builder = Plugin::instance()->modules_manager->get_modules( 'theme-builder' );
$is_saved = $theme_builder
->get_conditions_manager()
->save_conditions( $post_id, $conditions );
if ( ! $is_saved ) {
return new \WP_Error(
'conditions_save',
__( 'Cannot save those conditions.', 'elementor-pro' ),
[ 'status' => Exceptions::INTERNAL_SERVER_ERROR ]
);
}
return true;
}
}

View File

@@ -0,0 +1,238 @@
<?php
namespace ElementorPro\Core\App\Modules\SiteEditor\Data\Endpoints;
use ElementorPro\Plugin;
use Elementor\Core\Utils\Exceptions;
use Elementor\TemplateLibrary\Manager as TemplateManager;
use ElementorPro\Modules\ThemeBuilder\Documents\Theme_Document;
use ElementorPro\Modules\ThemeBuilder\Classes\Conditions_Manager;
use ElementorPro\Modules\ThemeBuilder\Module as ThemeBuilderModule;
use ElementorPro\Modules\ThemeBuilder\Classes\Templates_Types_Manager;
use ElementorPro\Core\App\Modules\SiteEditor\Render_Mode_Template_Preview;
use ElementorPro\Core\App\Modules\SiteEditor\Data\Responses\Lock_Error_Response;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Templates extends Base_Endpoint {
/**
* @var TemplateManager
*/
private $templates_manager;
/**
* @var array
*/
private $document_types;
public function __construct( $controller ) {
parent::__construct( $controller );
$this->templates_manager = Plugin::elementor()->templates_manager;
}
/**
* @return string
*/
public function get_name() {
return 'templates';
}
protected function register() {
parent::register();
$this->register_item_route( \WP_REST_Server::DELETABLE );
$this->register_item_route( \WP_REST_Server::EDITABLE );
$this->register_items_route( \WP_REST_Server::CREATABLE );
}
public function get_items( $request ) {
$templates = $this->templates_manager->get_source( 'local' )->get_items( [
'type' => array_keys( $this->get_documents_types() ),
'post_status' => 'any',
'orderby' => 'post_date',
'order' => 'DESC',
] );
return $this->normalize_templates_json( $templates );
}
public function create_items( $request ) {
$response = $this->templates_manager->import_template( $request->get_body_params() );
if ( is_wp_error( $response ) ) {
return new \WP_Error( 'file', $response->get_error_message(), [ 'status' => Exceptions::BAD_REQUEST ] );
}
return $this->normalize_templates_json( $response );
}
public function update_item( $id, $request ) {
$lock_by_user_id = $this->is_post_lock( $id );
if ( $lock_by_user_id ) {
return new Lock_Error_Response( $lock_by_user_id );
}
wp_update_post( array_merge( [
'ID' => $id,
], $request->get_body_params() ) );
return $this->normalize_template_json_item(
$this->templates_manager->get_source( 'local' )->get_item( $id )
);
}
public function delete_item( $id, $request ) {
$lock_by_user_id = $this->is_post_lock( $id );
if ( $lock_by_user_id ) {
return new Lock_Error_Response( $lock_by_user_id );
}
return ! ! wp_trash_post( $id );
}
/**
* @return array
*/
private function get_documents_types() {
if ( ! $this->document_types ) {
/** @var Templates_Types_Manager $types_manager */
$types_manager = ThemeBuilderModule::instance()->get_types_manager();
$this->document_types = $types_manager->get_types_config( [
'support_site_editor' => true,
] );
}
return $this->document_types;
}
/**
* @param $templates
*
* @return array
*/
private function normalize_templates_json( $templates ) {
return array_map( [ $this, 'normalize_template_json_item' ], $templates );
}
/**
* @param $template
*
* @return array
*/
private function normalize_template_json_item( $template ) {
/** @var Conditions_Manager $conditions_manager */
$conditions_manager = Plugin::instance()->modules_manager->get_modules( 'theme-builder' )->get_conditions_manager();
/** @var Theme_Document $document */
$document = Plugin::elementor()->documents->get( $template['template_id'] );
$supports_site_editor = $document::get_property( 'support_site_editor' );
// Supports also a non site editor parts.
if ( ! $supports_site_editor ) {
return [
'id' => $template['template_id'],
'url' => $template['url'],
'editURL' => $document->get_edit_url(),
'supportsSiteEditor' => false,
];
}
$types = $this->get_documents_types();
$template['instances'] = $conditions_manager->get_document_instances( $template['template_id'] );
$template['defaultCondition'] = $types[ $template['type'] ]['condition_type'];
$has_instances = ! empty( $template['instances'] );
$is_active = false;
if ( ! $has_instances ) {
$template['instances'] = [ 'no_instances' => esc_html__( 'No instances', 'elementor-pro' ) ];
} else {
$is_active = 'publish' === $template['status'];
}
if ( ! $template['thumbnail'] ) {
$template['thumbnail'] = '';
}
$site_editor_config = $document->get_site_editor_config();
$data = array_merge( $template, [
'id' => $template['template_id'],
'exportLink' => $template['export_link'],
'modifiedDate' => $template['human_modified_date'],
'editURL' => $document->get_edit_url(),
'conditions' => array_map( function ( $condition ) {
return array_merge( $condition, [
'sub' => $condition['sub_name'],
'subId' => $condition['sub_id'],
] );
}, $conditions_manager->get_document_conditions( $document ) ),
'isActive' => $is_active,
'type' => $this->calculate_template_type( $template['type'], $template['instances'] ),
'previewUrl' => $this->get_preview_url( $template['template_id'] ),
'placeholderUrl' => $site_editor_config['urls']['thumbnail'],
'pageLayout' => $site_editor_config['page_layout'],
'supportsSiteEditor' => true,
'showInstances' => $site_editor_config['show_instances'],
] );
/**
* Template data.
*
* Filters the data returned by Elementor API as JSON.
*
* By default Elementor API returns data in a JSON format that enables the
* builder to work properly. This hook allows developers to alter the data
* returned by the API to add new elements.
*
* @param array $data Template data.
*/
$data = apply_filters( 'elementor-pro/site-editor/data/template', $data );
return $data;
}
/**
* @param $type
* @param $instances
*
* @return string
*/
private function calculate_template_type( $type, $instances ) {
$condition_to_type_map = [
'front_page' => 'single-page',
'child_of' => 'single-page',
'page' => 'single-page',
'not_found404' => 'error-404',
'search' => 'search-results',
];
// "single" type was split into "single-page", "single-post" and "404".
// this section supports all the old templates that was created as "single".
if ( 'single' === $type ) {
// By default show it under single-post.
$type = 'single-post';
foreach ( $instances as $condition_name => $condition_label ) {
if ( isset( $condition_to_type_map[ $condition_name ] ) ) {
$type = $condition_to_type_map[ $condition_name ];
break;
}
}
}
return $type;
}
private function get_preview_url( $post_id ) {
return Render_Mode_Template_Preview::get_url( $post_id );
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace ElementorPro\Core\App\Modules\SiteEditor\Data\Responses;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Lock_Error_Response extends \WP_Error {
public function __construct( $user_id ) {
$user = get_user_by( 'ID', $user_id );
parent::__construct(
'post_lock',
sprintf(
/* translators: %s: User display name. */
esc_html__( '%s is currently editing this template, please try again later', 'elementor-pro' ),
$user->display_name
),
[
'status' => 403,
'locked_by_user_id' => $user_id,
'locked_by_user_name' => $user->display_name,
]
);
}
}

View File

@@ -0,0 +1,221 @@
<?php
namespace ElementorPro\Core\App\Modules\SiteEditor;
use Elementor\Core\Admin\Menu\Admin_Menu_Manager;
use Elementor\Core\Experiments\Manager as ExperimentsManager;
use Elementor\Core\Frontend\Render_Mode_Manager;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Common\Modules\Ajax\Module as Ajax;
use Elementor\TemplateLibrary\Source_Local;
use ElementorPro\Core\App\Modules\SiteEditor\Data\Controller;
use ElementorPro\Core\Behaviors\Feature_Lock;
use ElementorPro\Modules\ThemeBuilder\AdminMenuItems\Theme_Builder_Menu_Item;
use ElementorPro\Modules\ThemeBuilder\Module as Theme_Builder_Table_View;
use ElementorPro\Modules\ThemeBuilder\Module as ThemeBuilderModule;
use ElementorPro\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Site Editor Module
*
* Responsible for initializing Elementor Pro App functionality
*/
class Module extends BaseModule {
/**
* @var Feature_Lock
*/
private $lock;
/**
* Get name.
*
* @access public
*
* @return string
*/
public function get_name() {
return 'site-editor';
}
/**
* @throws \Exception
*/
public function get_template_types() {
// Same as admin menu capabilities.
if ( ! current_user_can( 'publish_posts' ) ) {
throw new \Exception( 'Access denied' );
}
$document_types = Plugin::elementor()->documents->get_document_types( [
'support_site_editor' => true,
] );
// Keep 404 at end of array.
$error_404 = $document_types['error-404'];
unset( $document_types['error-404'] );
$document_types['error-404'] = $error_404;
// Currently the `single` itself is not supported in site editor.
// Don't use `support_site_editor=false` in order to support documents that extend it.
unset( $document_types['single'] );
$types = [];
foreach ( $document_types as $type => $class ) {
$types[] = $class::get_site_editor_config();
}
return $types;
}
/**
* Register ajax actions.
*
* @access public
*
* @param Ajax $ajax
*/
public function register_ajax_actions( Ajax $ajax ) {
$ajax->register_ajax_action( 'app_site_editor_template_types', [ $this, 'get_template_types' ] );
}
/**
* @param Render_Mode_Manager $manager
*
* @throws \Exception
*/
public function register_render_mode( Render_Mode_Manager $manager ) {
$manager->register_render_mode( Render_Mode_Template_Preview::class );
}
protected function get_init_settings() {
$settings = [
'urls' => [
'legacy_view' => add_query_arg( 'tabs_group', ThemeBuilderModule::ADMIN_LIBRARY_TAB_GROUP, admin_url( Source_Local::ADMIN_MENU_SLUG ) ),
],
'utms' => [
'utm_source' => 'theme-builder',
'utm_medium' => 'wp-dash',
],
];
if ( $this->lock->is_locked() ) {
$settings['lock'] = $this->lock->get_config();
}
return $settings;
}
private function add_default_new_site_editor_experiments( ExperimentsManager $manager ) {
$manager->add_feature( [
'name' => 'theme_builder_v2',
'title' => __( 'Default to New Theme Builder', 'elementor-pro' ),
'description' => __( 'Entering the Theme Builder through WP Dashboard > Templates > Theme Builder opens the New theme builder by default. But dont worry, you can always view the WP styled version of the screen with a simple click of a button.', 'elementor-pro' ),
'release_status' => ExperimentsManager::RELEASE_STATUS_STABLE,
'default' => ExperimentsManager::STATE_ACTIVE,
] );
}
/**
* Get site editor url.
*
* @return string
*/
private function get_site_editor_url() : string {
return Plugin::elementor()->app->get_base_url() . '#/site-editor';
}
private function register_site_editor_menu() {
$experiments_manager = Plugin::elementor()->experiments;
// Unique case when the experiments manager is not initialized yet.
if ( ! $experiments_manager || ! $experiments_manager->is_feature_active( 'theme_builder_v2' ) ) {
return;
}
// Remove the old theme builder link and add the new one.
remove_submenu_page(
Source_Local::ADMIN_MENU_SLUG,
add_query_arg( 'tabs_group', ThemeBuilderModule::ADMIN_LIBRARY_TAB_GROUP, Source_Local::ADMIN_MENU_SLUG )
);
add_submenu_page(
Source_Local::ADMIN_MENU_SLUG,
'',
__( 'Theme Builder', 'elementor-pro' ),
'publish_posts',
$this->get_site_editor_url()
);
}
private function register_admin_menu( Admin_Menu_Manager $admin_menu_manager ) {
$experiments_manager = Plugin::elementor()->experiments;
// Unique case when the experiments manager is not initialized yet.
if ( ! $experiments_manager || ! $experiments_manager->is_feature_active( 'theme_builder_v2' ) ) {
return;
}
$admin_menu_manager->unregister( add_query_arg( 'tabs_group', ThemeBuilderModule::ADMIN_LIBRARY_TAB_GROUP, Source_Local::ADMIN_MENU_SLUG ) );
$admin_menu_manager->register(
$this->get_site_editor_url(),
new Theme_Builder_Menu_Item()
);
}
private function add_finder_item( array $categories ) {
if ( ! Plugin::elementor()->experiments->is_feature_active( 'theme_builder_v2' ) ) {
return $categories;
}
// Replace the old theme builder "create-new" link with the new site-editor.
$categories['create']['items']['theme-template'] = [
'title' => __( 'Add New Theme Template', 'elementor-pro' ),
'icon' => 'plus-circle-o',
'url' => $this->get_site_editor_url() . '/add-new',
'keywords' => [ 'template', 'theme', 'new', 'create' ],
];
return $categories;
}
/**
* Module constructor.
*
* @access public
*/
public function __construct() {
$this->lock = new Feature_Lock( [ 'type' => 'theme-builder' ] );
Plugin::elementor()->data_manager->register_controller( Controller::class );
add_action( 'elementor/ajax/register_actions', [ $this, 'register_ajax_actions' ], 11 /* Override core actions */ );
add_action( 'elementor/frontend/render_mode/register', [ $this, 'register_render_mode' ] );
add_action( 'elementor/experiments/default-features-registered', function ( ExperimentsManager $manager ) {
$this->add_default_new_site_editor_experiments( $manager );
} );
add_action( 'elementor/admin/menu/register', function ( Admin_Menu_Manager $admin_menu ) {
$this->register_admin_menu( $admin_menu );
}, Theme_Builder_Table_View::ADMIN_MENU_PRIORITY + 1 );
// TODO: BC - Remove after `Admin_Menu_Manager` will be the standard.
add_action( 'admin_menu', function () {
if ( did_action( 'elementor/admin/menu/register' ) ) {
return;
}
$this->register_site_editor_menu();
}, 23 /* After old theme builder */ );
add_filter( 'elementor/finder/categories', function ( array $categories ) {
return $this->add_finder_item( $categories );
}, 11 /* After old theme builder */ );
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace ElementorPro\Core\App\Modules\SiteEditor;
use Elementor\Core\Frontend\RenderModes\Render_Mode_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Render_Mode_Template_Preview extends Render_Mode_Base {
/**
* @return string
*/
public static function get_name() {
return 'template-preview';
}
public function filter_template() {
return ELEMENTOR_PATH . 'modules/page-templates/templates/canvas.php';
}
public function prepare_render() {
parent::prepare_render();
show_admin_bar( false );
remove_filter(
'the_content',
[ \ElementorPro\Modules\ThemeBuilder\Module::instance()->get_locations_manager(), 'builder_wrapper' ],
9999999
);
add_filter( 'template_include', [ $this, 'filter_template' ] );
add_action( 'wp_head', [ $this, 'render_pointer_event_style' ] );
}
/**
* disable all the interactions in the preview render mode.
*/
public function render_pointer_event_style() {
echo '<style> html { pointer-events: none; } </style>';
}
public function is_static() {
return true;
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace ElementorPro\Core\Behaviors;
use ElementorPro\License\API;
use ElementorPro\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Feature_Lock implements Temp_Lock_Behavior {
private $config;
public function __construct( $config = [] ) {
$this->config = $config;
}
public function is_locked() {
return ! API::is_license_active();
}
public function get_config() {
$utm_args = [
'utm_source' => '%%utm_source%%', // Will be replaced in the frontend.
'utm_medium' => '%%utm_medium%%',
'utm_campaign' => API::is_license_expired()
? 'renew-license'
: 'connect-and-activate-license',
'utm_term' => $this->config['type'],
];
$connect_url = Plugin::instance()->license_admin->get_connect_url( $utm_args );
$renew_url = add_query_arg( $utm_args, 'https://my.elementor.com/subscriptions/' );
return [
'is_locked' => $this->is_locked(),
'badge' => [
'icon' => 'eicon-lock',
'text' => esc_html__( 'Pro', 'elementor-pro' ),
],
'content' => [
'heading' => esc_html__( 'You need an active Elementor Pro license', 'elementor-pro' ),
'description' => esc_html__( 'Your Elementor Pro license is inactive. To access premium Elementor widgets, templates, support & plugin updates activate your Pro license.', 'elementor-pro' ),
],
'button' => [
'text' => API::is_license_expired()
? esc_html__( 'Renew now', 'elementor-pro' )
: esc_html__( 'Connect & Activate', 'elementor-pro' ),
'url' => API::is_license_expired()
? $renew_url
: $connect_url,
],
];
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace ElementorPro\Core\Behaviors;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
// TODO: Used here for testing. Should be removed when it'll be available in the Core.
interface Temp_Lock_Behavior {
/**
* @return bool
*/
public function is_locked();
/**
* @return array {
*
* @type bool $is_locked
*
* @type array $badge {
* @type string $icon
* @type string $text
* }
*
* @type array $content {
* @type string $heading
* @type string $description
* }
*
* @type array $button {
* @type string $text
* @type string $url
* }
*
* }
*/
public function get_config();
}

View File

@@ -0,0 +1,25 @@
<?php
namespace ElementorPro\Core\Compatibility;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Compatibility {
public static function register_actions() {
add_action( 'init', [ __CLASS__, 'on_init' ] );
}
public static function on_init() {
static::translate_press();
}
private static function translate_press() {
if ( ! class_exists( 'TRP_Translate_Press' ) ) {
return;
}
add_filter( 'elementor_pro/license/api/use_home_url', '__return_false' );
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace ElementorPro\Core\Connect\Apps;
use Elementor\Core\Common\Modules\Connect\Apps\Common_App;
use ElementorPro\License;
use ElementorPro\License\API;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Activate extends Common_App {
public function get_title() {
return esc_html__( 'Activate', 'elementor-pro' );
}
public function get_slug() {
return 'activate';
}
protected function after_connect() {
$this->action_activate_license();
}
/**
* @since 2.3.0
* @access public
*/
public function action_authorize() {
// In case the first connect was not from Activate App - require a new authorization.
if ( $this->is_connected() && ! License\Admin::get_license_key() ) {
$this->disconnect();
}
parent::action_authorize();
}
public function action_activate_pro() {
$this->action_activate_license();
}
public function action_switch_license() {
$this->disconnect();
$this->action_authorize();
}
public function action_deactivate() {
License\Admin::deactivate();
$this->disconnect();
wp_safe_redirect( License\Admin::get_url() );
die;
}
public function action_activate_license() {
if ( ! $this->is_connected() ) {
$this->add_notice( esc_html__( 'Please connect to Elementor in order to activate license.', 'elementor-pro' ), 'error' );
$this->redirect_to_admin_page();
}
$license = $this->request( 'get_connected_license' );
if ( empty( $license ) ) {
// TODO: add suggestions how to check/resolve.
wp_die( 'License not found for user ' . esc_attr( $this->get( 'user' )->email ), esc_html__( 'Elementor Pro', 'elementor-pro' ), [
'back_link' => true,
] );
}
if ( is_wp_error( $license ) ) {
wp_die( $license, esc_html__( 'Elementor Pro', 'elementor-pro' ), [ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
'back_link' => true,
] );
}
$license_key = trim( $license->key );
if ( empty( $license_key ) ) {
wp_die( esc_html__( 'License key is missing.', 'elementor-pro' ), esc_html__( 'Elementor Pro', 'elementor-pro' ), [
'back_link' => true,
] );
}
$data = License\API::activate_license( $license_key );
if ( is_wp_error( $data ) ) {
wp_die( sprintf( '%s (%s) ', $data->get_error_message(), $data->get_error_code() ), esc_html__( 'Elementor Pro', 'elementor-pro' ), [ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
'back_link' => true,
] );
}
if ( empty( $data['success'] ) ) {
$error_msg = License\API::get_error_message( $data['error'] ); // get_error_message() escapes html
wp_die( $error_msg, esc_html__( 'Elementor Pro', 'elementor-pro' ), [ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
'back_link' => true,
] );
}
License\Admin::set_license_key( $license_key );
License\API::set_license_data( $data );
$this->add_notice( esc_html__( 'License has been activated successfully.', 'elementor-pro' ) );
$this->redirect_to_admin_page( License\Admin::get_url() );
die;
}
public function action_reset() {
if ( current_user_can( 'manage_options' ) ) {
delete_option( 'elementor_pro_license_key' );
delete_transient( 'elementor_pro_license_data' );
}
$this->redirect_to_admin_page();
}
protected function get_popup_success_event_data() {
return [
'templates_access_level' => API::get_library_access_level( 'template' ),
'kits_access_level' => API::get_library_access_level( 'kit' ),
'access_tier' => API::get_access_tier(),
];
}
protected function get_app_info() {
return [
'license_data' => [
'label' => 'License Data',
'value' => get_option( '_elementor_pro_license_data' ),
],
'license_key' => [
'label' => 'License Key',
'value' => get_option( 'elementor_pro_license_key' ),
],
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace ElementorPro\Core\Connect;
use ElementorPro\Core\Connect\Apps\Activate;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Manager {
/**
* @param \Elementor\Core\Common\Modules\Connect\Module $apps_manager
*/
public function register_apps( $apps_manager ) {
$apps = [
'activate' => Activate::get_class_name(),
];
foreach ( $apps as $slug => $class ) {
$apps_manager->register_app( $slug, $class );
}
}
public function __construct() {
add_action( 'elementor/connect/apps/register', [ $this, 'register_apps' ] );
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace ElementorPro\Core\Database;
use ElementorPro\Core\Utils\Collection;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Base_Database_Updater {
/**
* Run all the 'up' method of the migrations classes if needed, and update the db version.
*
* @param bool $force When passing true, it ignores the current version and run all the up migrations.
*/
public function up( $force = false ) {
$installed_version = $this->get_installed_version();
// Up to date. Nothing to do.
if ( ! $force && $this->get_db_version() <= $installed_version ) {
return;
}
$migrations = $this->get_collected_migrations();
if ( ! $force ) {
$migrations = $migrations->filter( function ( $_, $version ) use ( $installed_version ) {
// Filter all the migrations that already done.
return $version > $installed_version;
} );
}
$migrations->map( function ( Base_Migration $migration, $version ) {
$migration->up();
// In case some migration failed it updates version every migration.
$this->update_db_version_option( $version );
} );
$this->update_db_version_option( $this->get_db_version() );
}
/**
* Run all the 'down' method of the migrations classes if can, and update the db version.
*
* @param bool $force When passing true, it ignores the current version and run all the down migrations.
*/
public function down( $force = false ) {
$installed_version = $this->get_installed_version();
$migrations = $this->get_collected_migrations();
if ( ! $force ) {
$migrations = $migrations->filter( function ( $_, $version ) use ( $installed_version ) {
// Filter all the migrations that was not installed.
return $version <= $installed_version;
} );
}
$migrations->reverse( true )
->map( function ( Base_Migration $migration, $version ) {
$migration->down();
// In case some migration failed it updates version every migration.
$this->update_db_version_option( $version );
} );
$this->update_db_version_option( 0 );
}
/**
* Register hooks to activate the migrations.
*/
public function register() {
add_action( 'admin_init', function () {
$this->up();
} );
}
/**
* Update the version in the users DB.
*
* @param $version
*/
protected function update_db_version_option( $version ) {
update_option( $this->get_db_version_option_name(), $version );
}
/**
* Get the version that already installed.
*
* @return int
*/
protected function get_installed_version() {
return intval( get_option( $this->get_db_version_option_name() ) );
}
/**
* Get all migrations inside a Collection.
*
* @return Collection
*/
protected function get_collected_migrations() {
return new Collection( $this->get_migrations() );
}
/**
* The most updated version of the DB.
*
* @return numeric
*/
abstract protected function get_db_version();
/**
* The name of the option that saves the current user DB version.
*
* @return string
*/
abstract protected function get_db_version_option_name();
/**
* Array of migration classes.
*
* @return Base_Migration[]
*/
abstract protected function get_migrations();
}

View File

@@ -0,0 +1,182 @@
<?php
namespace ElementorPro\Core\Database;
use Elementor\Core\Utils\Collection;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Base_Migration {
/*
* @see https://github.com/WordPress/WordPress/blob/d2694aa46647af48d1bcaff48a4f6cac7f5cf470/wp-admin/includes/schema.php#L49
*/
const MAX_INDEX_LENGTH = 191;
/**
* @var \wpdb
*/
protected $wpdb;
/**
* @param \wpdb|null $wpdb_instance
*/
public function __construct( \wpdb $wpdb_instance = null ) {
if ( ! $wpdb_instance ) {
global $wpdb;
$this->wpdb = $wpdb;
} else {
$this->wpdb = $wpdb_instance;
}
}
/**
* Runs when upgrading the database
*
* @return void
*/
abstract public function up();
/**
* Runs when downgrading the database.
*
* @return void
*/
abstract public function down();
/**
* A util to run SQL for creating tables.
*
* @param $table_name
* @param array $columns
*/
protected function create_table( $table_name, array $columns ) {
$table_name = "{$this->wpdb->prefix}{$table_name}";
$columns_sql = ( new Collection( $columns ) )
->map( function( $definition, $col_name ) {
return "`{$col_name}` {$definition}";
} )
->implode( ', ' );
$query = "CREATE TABLE `{$table_name}` ({$columns_sql}) {$this->wpdb->get_charset_collate()};";
$this->run_db_delta( $query );
}
/**
* Add columns.
*
* @param $table_name
* @param array $columns
*/
protected function add_columns( $table_name, array $columns ) {
$table_name = "{$this->wpdb->prefix}{$table_name}";
$add_columns_sql = ( new Collection( $columns ) )
->map( function ( $definition, $column_name ) {
return "ADD COLUMN `{$column_name}` {$definition}";
} )
->implode( ', ' );
$this->wpdb->query( "ALTER TABLE `{$table_name}` {$add_columns_sql};" ); // phpcs:ignore
}
/**
* Drop columns
*
* @param $table_name
* @param array $columns
*/
protected function drop_columns( $table_name, array $columns ) {
$table_name = "{$this->wpdb->prefix}{$table_name}";
$drop_columns_sql = ( new Collection( $columns ) )
->map( function ( $column_name ) {
return "DROP COLUMN `{$column_name}`";
} )
->implode( ', ' );
$this->wpdb->query( "ALTER TABLE `{$table_name}` {$drop_columns_sql};" ); // phpcs:ignore
}
/**
* A util to run SQL for dropping tables.
*
* @param $table_name
*/
protected function drop_table( $table_name ) {
$table_name = "{$this->wpdb->prefix}{$table_name}";
$query = "DROP TABLE IF EXISTS `{$table_name}`;";
// Safe query that shouldn't be escaped.
$this->wpdb->query( $query ); // phpcs:ignore
}
/**
* A util to run SQL for creating indexes.
*
* @param $table_name
* @param array $column_names
*/
protected function create_indexes( $table_name, array $column_names ) {
$max_index_length = static::MAX_INDEX_LENGTH;
$table_name = "{$this->wpdb->prefix}{$table_name}";
// Safe query that shouldn't be escaped.
$column_definition = $this->get_column_definition( $table_name );
if ( ! $column_definition ) {
return;
}
$should_set_max_length_index = ( new Collection( $column_definition ) )
->filter( function ( $value ) {
preg_match( '/\((\d+)\)/', $value['Type'], $match );
return ( isset( $match[1] ) && intval( $match[1] ) > Base_Migration::MAX_INDEX_LENGTH )
|| in_array( strtolower( $value['Type'] ), [ 'text', 'longtext' ], true );
} )
->pluck( 'Field' )
->values();
$indexes_sql = ( new Collection( $column_names ) )
->map( function( $col_name ) use ( $should_set_max_length_index, $max_index_length ) {
$max_index_length_sql = '';
if ( in_array( $col_name, $should_set_max_length_index, true ) ) {
$max_index_length_sql = " ({$max_index_length})";
}
return "ADD INDEX `{$col_name}_index` (`{$col_name}`{$max_index_length_sql})";
} )
->implode( ', ' );
// Safe query that shouldn't be escaped.
$this->wpdb->query( "ALTER TABLE `{$table_name}` {$indexes_sql};" ); // phpcs:ignore
}
/**
* @param $table_name
*
* @return array
*/
protected function get_column_definition( $table_name ) {
return $this->wpdb->get_results( "SHOW COLUMNS FROM `{$table_name}`;", ARRAY_A ); // phpcs:ignore
}
/**
* Runs global dbDelta function (wrapped into method to allowing mock for testing).
*
* @param $query
*
* @return array
*/
protected function run_db_delta( $query ) {
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
return dbDelta( $query );
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace ElementorPro\Core\Database;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* JOIN clause builder.
*
* Essentially, it uses the regular Builder's capabilities while wrapping some method
* for syntactic sugar and better readability.
*/
class Join_Clause extends Query_Builder {
// JOIN types.
const TYPE_INNER = 'inner';
const TYPE_LEFT = 'left';
const TYPE_RIGHT = 'right';
/**
* JOIN type.
*
* @var string
*/
public $type;
/**
* Join_Clause constructor.
*
* @param string $type - JOIN type.
* @param \wpdb|null $connection - MySQL connection to use.
*
* @return void
*/
public function __construct( $type, \wpdb $connection = null ) {
parent::__construct( $connection );
$this->type = $type;
}
/**
* @uses `$this->where()`.
*
* @return Join_Clause
*/
public function on( $column, $operator, $value, $and_or = self::RELATION_AND ) {
return $this->where( $column, $operator, $value, $and_or );
}
/**
* @shortcut `$this->on()`.
*
* @return Join_Clause
*/
public function or_on( $first, $operator, $second ) {
return $this->on( $first, $operator, $second, self::RELATION_OR );
}
/**
* @uses `$this->where_column()`.
*
* @return Join_Clause
*/
public function on_column( $first, $operator, $second, $and_or = self::RELATION_AND ) {
return $this->where_column( $first, $operator, $second, $and_or );
}
/**
* @shortcut `$this->on_column()`.
*
* @return Join_Clause
*/
public function or_on_column( $first, $operator, $second ) {
return $this->on_column( $first, $operator, $second, self::RELATION_OR );
}
}

View File

@@ -0,0 +1,160 @@
<?php
namespace ElementorPro\Core\Database;
use ElementorPro\Core\Utils\Collection;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Model_Base implements \JsonSerializable {
// Casting types.
const TYPE_BOOLEAN = 'boolean';
const TYPE_COLLECTION = 'collection';
const TYPE_INTEGER = 'integer';
const TYPE_STRING = 'string';
const TYPE_JSON = 'json';
const TYPE_DATETIME = 'datetime';
const TYPE_DATETIME_GMT = 'datetime_gmt';
/**
* Casts array.
* Used to automatically cast values from DB to the appropriate property type.
*
* @var array
*/
protected static $casts = [];
/**
* Model_Base constructor.
*
* @param array $fields - Fields from the DB to fill.
*
* @return void
*/
public function __construct( array $fields ) {
foreach ( $fields as $key => $value ) {
if ( ! property_exists( $this, $key ) ) {
continue;
}
$this->{$key} = ( empty( static::$casts[ $key ] ) )
? $value
: static::cast( $value, static::$casts[ $key ] );
}
}
/**
* Get the model's table name.
* Throws an exception by default in order to require implementation,
* since abstract static functions are not allowed.
*
* @return string
*/
public static function get_table() {
throw new \Exception( 'You must implement `get_table()` inside ' . static::class );
}
/**
* Create a Query Builder for the model's table.
*
* @param \wpdb|null $connection - MySQL connection to use.
*
* @return Query_Builder
*/
public static function query( \wpdb $connection = null ) {
$builder = new Model_Query_Builder( static::class, $connection );
return $builder->from( static::get_table() );
}
/**
* Cast value into specific type.
*
* @param $value - Value to cast.
* @param $type - Type to cast into.
*
* @return mixed
*/
protected static function cast( $value, $type ) {
if ( null === $value ) {
return null;
}
switch ( $type ) {
case self::TYPE_BOOLEAN:
return boolval( $value );
case self::TYPE_COLLECTION:
return new Collection( $value );
case self::TYPE_INTEGER:
return intval( $value );
case self::TYPE_STRING:
return strval( $value );
case self::TYPE_JSON:
return json_decode( $value, true );
case self::TYPE_DATETIME:
return new \DateTime( $value );
case self::TYPE_DATETIME_GMT:
return new \DateTime( $value, new \DateTimeZone( 'GMT' ) );
}
return $value;
}
/**
* Cast a model property value into a JSON compatible data type.
*
* @param $value - Value to cast.
* @param $type - Type to cast into.
* @param $property_name - The model property name.
*
* @return mixed
*/
protected static function json_serialize_property( $value, $type, $property_name ) {
switch ( $type ) {
case self::TYPE_DATETIME:
case self::TYPE_DATETIME_GMT:
/** @var \DateTime $value */
return $value->format( 'c' );
}
/** @var mixed $value */
return $value;
}
/**
* @return array
*/
#[\ReturnTypeWillChange]
public function jsonSerialize() {
return ( new Collection( (array) $this ) )
->map( function ( $_, $key ) {
$value = $this->{$key};
$type = array_key_exists( $key, static::$casts )
? static::$casts[ $key ]
: null;
if ( null === $value ) {
return $value;
}
// Can be overridden by child model.
$value = static::json_serialize_property( $value, $type, $key );
if ( $value instanceof \JsonSerializable ) {
return $value->jsonSerialize();
}
return $value;
} )
->all();
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace ElementorPro\Core\Database;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Model_Query_Builder extends Query_Builder {
/**
* The Query Builder associated model.
*
* @var string
*/
public $model;
/**
* Whether the returned value should be hydrated into a model.
*
* @var bool
*/
public $return_as_model = true;
/**
* Model_Query_Builder constructor.
*
* @param string $model_classname - Model to use inside the builder.
* @param \wpdb|null $connection - MySQL connection.
*/
public function __construct( $model_classname, \wpdb $connection = null ) {
$this->set_model( $model_classname );
parent::__construct( $connection );
}
/**
* Set the model the generated from the query builder.
*
* @param $model_classname
*
* @return $this
*/
public function set_model( $model_classname ) {
$this->model = $model_classname;
return $this;
}
/**
* Disable model hydration.
*
* @return $this
*/
public function disable_model_initiation() {
$this->return_as_model = false;
return $this;
}
/**
* Disable hydration before calling the original count.
*
* @param string $column
*
* @return int
*/
public function count( $column = '*' ) {
$this->disable_model_initiation();
return parent::count( $column );
}
/**
* Disable hydration before calling the original pluck.
*
* @inheritDoc
*/
public function pluck( $column = null ) {
$this->disable_model_initiation();
return parent::pluck( $column );
}
/**
* Override the parent `get()` and make Models from the results.
*
* @return \ElementorPro\Core\Utils\Collection
*/
public function get() {
$items = parent::get();
if ( ! $this->return_as_model ) {
return $items;
}
// Convert the SQL results to Model instances.
return $items->map( function ( $comment ) {
return new $this->model( $comment );
} );
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,189 @@
<?php
namespace ElementorPro\Core\Editor;
use Elementor\Core\Base\App;
use Elementor\Core\Utils\Assets_Config_Provider;
use Elementor\Core\Utils\Assets_Translation_Loader;
use ElementorPro\License\Admin as License_Admin;
use ElementorPro\License\API as License_API;
use ElementorPro\Plugin;
use ElementorPro\Modules\DisplayConditions\Module as Display_Conditions_Module;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Editor extends App {
const EDITOR_V2_PACKAGES = [
'editor-documents-extended',
'editor-site-navigation-extended',
];
/**
* Get app name.
*
* Retrieve the app name.
*
* @return string app name.
* @since 2.6.0
* @access public
*
*/
public function get_name() {
return 'pro-editor';
}
public function __construct() {
add_action( 'elementor/init', [ $this, 'on_elementor_init' ] );
add_action( 'elementor/editor/init', [ $this, 'on_elementor_editor_init' ] );
add_action( 'elementor/editor/after_enqueue_styles', [ $this, 'enqueue_editor_styles' ] );
add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'enqueue_editor_scripts' ] );
add_filter( 'elementor/editor/localize_settings', [ $this, 'localize_settings' ] );
add_filter( 'elementor/editor/panel/get_pro_details', function( $get_pro_details ) {
if ( defined( '\Elementor\Modules\Apps\Module::PAGE_ID' ) ) {
$get_pro_details['link'] = admin_url( 'admin.php?page=' . \Elementor\Modules\Apps\Module::PAGE_ID );
$get_pro_details['message'] = __( 'Extend Elementor With Add-ons', 'elementor-pro' );
$get_pro_details['button_text'] = __( 'Explore Add-ons', 'elementor-pro' );
}
return $get_pro_details;
} );
add_action( 'elementor/editor/v2/scripts/enqueue', function () {
$this->enqueue_editor_v2_scripts();
} );
}
public function get_init_settings() {
$settings = [
'isActive' => License_API::is_license_active(),
'urls' => [
'modules' => ELEMENTOR_PRO_MODULES_URL,
'connect' => License_Admin::get_url(),
],
];
/**
* Localized editor settings.
*
* Filters the localized settings used in the editor as JavaScript variables.
*
* By default Elementor Pro passes some editor settings to be consumed as JavaScript
* variables. This hook allows developers to add extra settings values to be consumed
* using JavaScript in the editor.
*
* @since 1.0.0
*
* @param array $settings Localized editor settings.
*/
$settings = apply_filters( 'elementor_pro/editor/localize_settings', $settings );
return $settings;
}
public function enqueue_editor_styles() {
wp_enqueue_style(
'elementor-pro',
$this->get_css_assets_url( 'editor', null, 'default', true ),
[
'elementor-editor',
],
ELEMENTOR_PRO_VERSION
);
}
public function enqueue_editor_scripts() {
wp_enqueue_script(
'elementor-pro',
$this->get_js_assets_url( 'editor' ),
[
'backbone-marionette',
'elementor-common',
'elementor-editor-modules',
'elementor-editor-document',
],
ELEMENTOR_PRO_VERSION,
true
);
wp_set_script_translations( 'elementor-pro', 'elementor-pro' );
$this->print_config( 'elementor-pro' );
}
public function enqueue_editor_v2_scripts() {
$assets_config = ( new Assets_Config_Provider() )
->set_path_resolver( function ( $name ) {
return ELEMENTOR_PRO_ASSETS_PATH . "js/packages/{$name}/{$name}.asset.php";
} );
$packages = apply_filters( 'elementor-pro/editor/v2/packages', self::EDITOR_V2_PACKAGES );
foreach ( $packages as $package ) {
$assets_config->load( $package );
}
foreach ( $assets_config->all() as $package => $config ) {
wp_enqueue_script(
$config['handle'],
$this->get_js_assets_url( "packages/{$package}/{$package}" ),
$config['deps'],
ELEMENTOR_PRO_VERSION,
true
);
wp_set_script_translations( $config['handle'], 'elementor-pro' );
}
if ( class_exists( Assets_Translation_Loader::class ) ) {
$packages_handles = $assets_config->pluck( 'handle' )->all();
Assets_Translation_Loader::for_handles( $packages_handles );
}
}
public function localize_settings( array $settings ) {
$settings['elementPromotionURL'] = Plugin::instance()->license_admin->get_connect_url([
'utm_source' => '%s', // Will be replaced in the frontend to the widget name
'utm_medium' => 'wp-dash',
'utm_campaign' => 'connect-and-activate-license',
'utm_content' => 'editor-widget-promotion',
]);
$settings['dynamicPromotionURL'] = Plugin::instance()->license_admin->get_connect_url( [
'utm_source' => '%s', // Will be replaced in the frontend to the control name
'utm_medium' => 'wp-dash',
'utm_campaign' => 'connect-and-activate-license',
'utm_content' => 'editor-dynamic-promotion',
] );
if ( ! isset( $settings['promotionWidgets'] ) ) {
$settings['promotionWidgets'] = License_API::get_promotion_widgets();
}
if ( Display_Conditions_Module::can_use_display_conditions() && Display_Conditions_Module::is_experiment_active() ) {
$settings['displayConditions'] = Display_Conditions_Module::instance()
->get_conditions_manager()
->get_conditions_config();
}
return $settings;
}
public function on_elementor_init() {
Plugin::elementor()->editor->notice_bar = new Notice_Bar();
if ( isset( Plugin::elementor()->editor->promotion ) ) {
Plugin::elementor()->editor->promotion = new Promotion();
}
}
public function on_elementor_editor_init() {
Plugin::elementor()->common->add_template( __DIR__ . '/template.php' );
}
protected function get_assets_base_url() {
return ELEMENTOR_PRO_URL;
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace ElementorPro\Core\Editor;
use Elementor\Core\Editor\Notice_Bar as Base_Notice_Bar;
use ElementorPro\License\Admin;
use ElementorPro\License\API as License_API;
use ElementorPro\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Notice_Bar extends Base_Notice_Bar {
const ELEMENTOR_PRO_EDITOR_GO_PRO_TRIAL_ABOUT_TO_EXPIRE_LICENSE_NOTICE_DISMISSED = '_elementor_pro_editor_go_pro_trial_about_to_expire_license_notice_dismissed';
const ELEMENTOR_PRO_EDITOR_GO_PRO_TRIAL_EXPIRED_LICENSE_NOTICE_DISMISSED = '_elementor_pro_editor_go_pro_trial_expired_license_notice_dismissed';
const ELEMENTOR_PRO_EDITOR_RENEW_LICENSE_NOTICE_DISMISSED = '_elementor_pro_editor_renew_license_notice_dismissed';
const ELEMENTOR_PRO_EDITOR_ACTIVATE_LICENSE_NOTICE_DISMISSED = '_elementor_pro_editor_activate_license_notice_dismissed';
const ELEMENTOR_PRO_EDITOR_RENEW_ABOUT_TO_EXPIRE_LICENSE_NOTICE_DISMISSED = '_elementor_pro_editor_renew_about_to_expire_license_notice_dismissed';
protected function get_init_settings() {
$license_data = License_API::get_license_data();
$license_admin = Plugin::instance()->license_admin;
if ( License_API::is_license_active() && License_API::is_licence_pro_trial() ) {
return [
'option_key' => self::ELEMENTOR_PRO_EDITOR_GO_PRO_TRIAL_ABOUT_TO_EXPIRE_LICENSE_NOTICE_DISMISSED,
'message' =>
esc_html__( 'Heads up! You are using a free trial. Want to enjoy Pro widgets & templates for a whole year?', 'elementor-pro' )
. sprintf( ' <a href="https://my.elementor.com/upgrade-subscription/?utm_source=editor-notice-bar&utm_medium=wp-dash&utm_campaign=pro-trial&utm_content=trial-period" target="_blank">%s</a>', esc_html__( 'Go Pro now', 'elementor-pro' ) ),
'action_title' => '',
'action_url' => '',
'muted_period' => 0,
];
}
if ( License_API::is_license_expired() && License_API::is_licence_pro_trial() ) {
return [
'option_key' => self::ELEMENTOR_PRO_EDITOR_GO_PRO_TRIAL_EXPIRED_LICENSE_NOTICE_DISMISSED,
'message' => esc_html__( 'Your trial has expired. Miss your favorite Elementor Pro features?', 'elementor-pro' )
. sprintf( ' <a href="https://my.elementor.com/upgrade-subscription/?utm_source=editor-notice-bar&utm_medium=wp-dash&utm_campaign=pro-trial&utm_content=trial-expired" target="_blank">%s</a>', esc_html__( 'Upgrade now', 'elementor-pro' ) ),
'action_title' => '',
'action_url' => '',
'muted_period' => 0,
];
}
if ( License_API::is_license_expired() ) {
return [
'option_key' => self::ELEMENTOR_PRO_EDITOR_RENEW_LICENSE_NOTICE_DISMISSED,
'icon' => 'eicon-lock',
'message' => esc_html__(
'Renew to unlock all Elementor Pro features',
'elementor-pro'
),
'action_title' => esc_html__( 'Renew now', 'elementor-pro' ),
'action_url' => 'https://go.elementor.com/editor-notice-bar-renew/',
'secondary_message' => esc_html__(
'Already renewed?',
'elementor-pro'
),
'secondary_action_title' => esc_html__( 'Reload Editor', 'elementor-pro' ),
'secondary_action_url' => Admin::get_url() . '&redirect-to-document=' . Plugin::elementor()->documents->get_current()->get_id(),
'secondary_action_target' => '_self',
'muted_period' => 0,
];
}
if ( ! License_API::is_license_active() ) {
return [
'option_key' => self::ELEMENTOR_PRO_EDITOR_ACTIVATE_LICENSE_NOTICE_DISMISSED,
'message' => esc_html__( 'Activate Your License and Get Access to Premium Elementor Templates, Support & Plugin Updates.', 'elementor-pro' ),
'action_title' => esc_html__( 'Connect & Activate', 'elementor-pro' ),
'action_url' => $license_admin->get_connect_url( [
'mode' => 'popup',
'callback_id' => 'editor-pro-activate',
// UTM
'utm_source' => 'editor-notice-bar',
'utm_medium' => 'wp-dash',
'utm_campaign' => 'connect-and-activate-license',
] ),
'muted_period' => 0,
];
}
if ( ! License_API::is_license_about_to_expire() ) {
return [];
}
if ( isset( $license_data['renewal_discount'] ) && 0 < $license_data['renewal_discount'] ) {
$message = sprintf(
/* translators: %s: Renewal discount. */
esc_html__( 'Your Elementor Pro license is about to expire. Renew now and get an exclusive, time-limited %s discount.', 'elementor-pro' ),
$license_data['renewal_discount'] . '&#37;'
);
} else {
$message = esc_html__( 'Your Elementor Pro license is about to expire. Renew now and get updates, support, Pro widgets & templates for another year.', 'elementor-pro' );
}
return [
'option_key' => self::ELEMENTOR_PRO_EDITOR_RENEW_ABOUT_TO_EXPIRE_LICENSE_NOTICE_DISMISSED,
'message' => $message,
'action_title' => esc_html__( 'Renew now', 'elementor-pro' ),
'action_url' => 'https://go.elementor.com/editor-notice-bar-renew/',
'muted_period' => 1,
];
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace ElementorPro\Core\Editor;
use ElementorPro\License\API;
use ElementorPro\License\Admin;
use Elementor\Core\Editor\Promotion as Base_Promotion;
use ElementorPro\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Promotion extends Base_Promotion {
public function get_elements_promotion() {
if ( API::is_need_to_show_upgrade_promotion() ) {
return $this->get_elements_promotion__higher_tiers();
}
if ( API::is_license_active() ) {
return parent::get_elements_promotion();
}
return $this->get_elements_promotion__default();
}
private function get_elements_promotion__default() {
$is_license_expired = API::is_license_expired();
return [
/* translators: %s: Widget title. */
'title' => __( '%s Widget', 'elementor-pro' ),
'content' => $is_license_expired
/* translators: %s: Widget title. */
? __(
'Renew your Elementor Pro subscription to get %s and dozens more Pro widgets to expand your web-creation toolbox.',
'elementor-pro'
)
/* translators: %s: Widget title. */
: __(
'Use %s widget and dozens more pro features to extend your toolbox and build sites faster and better.',
'elementor-pro'
),
'action_button' => $is_license_expired ? [
'text' => __( 'Renew now', 'elementor-pro' ),
'url' => 'https://my.elementor.com/subscriptions/?utm_source=%s-pro-widget&utm_medium=wp-dash&utm_campaign=renew-license',
'classes' => [ 'elementor-button', 'elementor-button-brand' ],
] : [
'text' => __( 'Connect & Activate', 'elementor-pro' ),
'url' => Admin::get_url(),
],
];
}
private function get_elements_promotion__higher_tiers() {
return [
/* translators: %s: Widget title. */
'title' => __( '%s Widget', 'elementor-pro' ),
/* translators: %s: Widget title. */
'content' => __( 'Upgrade to Elementor Pro Advanced to get the %s widget as well as additional professional and ecommerce widgets.', 'elementor-pro' ),
'action_button' => [
'text' => __( 'Upgrade now', 'elementor-pro' ),
'url' => 'https://go.elementor.com/go-pro-advanced-%s',
'classes' => [ 'elementor-button', 'elementor-button-brand', 'go-pro' ],
],
];
}
}

View File

@@ -0,0 +1,16 @@
<?php
use ElementorPro\License\Admin as LicenseAdmin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
?>
<script type="text/template" id="tmpl-elementor-pro-template-library-activate-license-button">
<a class="elementor-template-library-template-action elementor-button go-pro" href="<?php
// PHPCS - the function LicenseAdmin::get_url() is safe.
echo LicenseAdmin::get_url(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" target="_blank">
<i class="eicon-external-link-square"></i>
<span class="elementor-button-title"><?php echo esc_html__( 'Activate License', 'elementor-pro' ); ?></span>
</a>
</script>

View File

@@ -0,0 +1,47 @@
<?php
namespace ElementorPro\Core\Integrations\Actions;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
abstract class Action_Base {
/**
* Validate a payload.
*
* @param mixed $payload - Payload object instance.
*
* @throws \ElementorPro\Core\Integrations\Exceptions\Action_Validation_Failed_Exception
*
* @return mixed
*/
abstract public function validate( $payload );
/**
* Apply the action.
*
* @param mixed $payload - Payload object instance.
*
* @throws \ElementorPro\Core\Integrations\Exceptions\Action_Failed_Exception
*
* @return void
*/
abstract public function apply( $payload );
/**
* Run the action.
*
* @param mixed $payload - Payload object instance.
*
* @throws \ElementorPro\Core\Integrations\Exceptions\Action_Validation_Failed_Exception
* @throws \ElementorPro\Core\Integrations\Exceptions\Action_Failed_Exception
*
* @return void
*/
public function run( $payload ) {
$this->validate( $payload );
$this->apply( $payload );
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace ElementorPro\Core\Integrations\Actions\Email;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Email_Address {
/**
* Recipient email address.
*
* @var array
*/
public $address;
/**
* Recipient name.
*
* @var string
*/
public $name;
/**
* Email_Address constructor.
*
* @param string $address
* @param string $name
*
* @return void
*/
public function __construct( $address, $name ) {
$this->address = (string) $address;
$this->name = (string) $name;
}
/**
* Format an email to be ready for header (e.g. `Recipient Name <user@email.com>` or `user@email.com`)
*
* @return string
*/
public function format() {
if ( ! empty( $this->name ) ) {
return sprintf( '%s <%s>', $this->name, $this->address );
}
return sprintf( '%s', $this->address );
}
}

View File

@@ -0,0 +1,239 @@
<?php
namespace ElementorPro\Core\Integrations\Actions\Email;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Email_Message {
/**
* Email sender.
*
* @var Email_Address
*/
public $from;
/**
* Email recipient.
*
* @var Email_Address
*/
public $to;
/**
* Email reply to address.
*
* @var Email_Address[]
*/
public $reply_to = [];
/**
* Email CC recipient.
*
* @var Email_Address[]
*/
public $cc = [];
/**
* Email BCC recipient.
*
* @var Email_Address[]
*/
public $bcc = [];
/**
* Email subject.
*
* @var string
*/
public $subject;
/**
* Email content type.
*
* @var string
*/
public $content_type;
/**
* Email body.
*
* @var string
*/
public $body;
/**
* Email attachments.
*
* @var array
*/
public $attachments = [];
/**
* Email_Message constructor.
*
* @return void
*/
public function __construct() {
// Set defaults.
$this->from( get_bloginfo( 'admin_email' ), get_bloginfo( 'name' ) );
}
/**
* Set the email sender.
*
* @param string $email
* @param string|null $name
*
* @return $this
*/
public function from( $email, $name = null ) {
$this->from = new Email_Address( $email, $name );
return $this;
}
/**
* Set the email recipient.
*
* @param string $email
* @param string|null $name
*
* @return $this
*/
public function to( $email, $name = null ) {
$this->to = new Email_Address( $email, $name );
return $this;
}
/**
* Add a reply to.
*
* @param string $email
* @param string|null $name
*
* @return $this
*/
public function reply_to( $email, $name = null ) {
$this->reply_to[] = new Email_Address( $email, $name );
return $this;
}
/**
* Add a CC.
*
* @param string $email
* @param string|null $name
*
* @return $this
*/
public function cc( $email, $name = null ) {
$this->cc[] = new Email_Address( $email, $name );
return $this;
}
/**
* Add a BCC.
*
* @param string $email
* @param string|null $name
*
* @return $this
*/
public function bcc( $email, $name = null ) {
$this->bcc[] = new Email_Address( $email, $name );
return $this;
}
/**
* Set the email subject.
*
* @param string $subject
*
* @return $this
*/
public function subject( $subject ) {
$this->subject = (string) $subject;
return $this;
}
/**
* Set the email content type.
*
* @param string $content_type
*
* @return $this
*/
public function content_type( $content_type ) {
$this->content_type = (string) $content_type;
return $this;
}
/**
* Set the email body using plain text.
*
* @param string $body
* @param string $content_type
*
* @return $this
*/
public function body( $body, $content_type = 'text/html' ) {
$this->body = (string) $body;
return $this->content_type( $content_type );
}
/**
* Set the email body using a view.
*
* @param string $path - View path,
* @param array $data - Data that will be passes to the view.
*
* @return $this
* @throws \Exception
*/
public function view( $path, $data = [] ) {
if ( ! is_file( $path ) ) {
throw new \Exception( "`{$path}` is not a valid view." );
}
ob_start();
// Inspired from Laravel's view mechanism:
// [1] https://github.dev/illuminate/filesystem/blob/b179f9ea3b3195d1f4b5ae2aee67e42eac6ceb5e/Filesystem.php#L98
// [2] https://github.dev/illuminate/view/blob/6dd315634a44450c5e443fa8735d4a526833fad3/Engines/PhpEngine.php#L48
call_user_func( function( $__view_path, $__view_data ) {
extract( $__view_data, EXTR_SKIP ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
unset( $__view_data );
// `$__view_data` keys are available in the file as variables.
require $__view_path;
}, $path, $data );
$this->body = ob_get_clean();
return $this->content_type( 'text/html' );
}
/**
* Add an attachment.
*
* @param string $path - Attachment path on the server.
*
* @return $this
*/
public function attach( $path ) {
$this->attachments[] = (string) $path;
return $this;
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace ElementorPro\Core\Integrations\Actions\Email;
use ElementorPro\Core\Integrations\Actions\Action_Base;
use ElementorPro\Core\Integrations\Exceptions\Action_Failed_Exception;
use ElementorPro\Core\Integrations\Exceptions\Action_Validation_Failed_Exception;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Email extends Action_Base {
/**
* @param Email_Message $payload
*
* @return void
* @throws \Exception
*/
public function apply( $payload ) {
// Set default headers.
$headers = [
sprintf( 'Content-Type: %s; charset=UTF-8', $payload->content_type ),
sprintf( 'From: %s', $payload->from->format() ),
];
foreach ( $payload->reply_to as $recipient ) {
$headers[] = sprintf( 'Reply-To: %s', $recipient->format() );
}
// Set CC headers.
$cc_headers = [];
foreach ( $payload->cc as $recipient ) {
$cc_headers[] = sprintf( 'Cc: %s', $recipient->format() );
}
// Send email.
$this->send_mail(
$payload->to->format(),
$payload->subject,
$payload->body,
implode( PHP_EOL, array_merge( $headers, $cc_headers ) ),
$payload->attachments
);
// Send BCC emails.
foreach ( $payload->bcc as $bcc ) {
$this->send_mail(
$bcc->format(),
$payload->subject,
$payload->body,
implode( PHP_EOL, $headers ),
$payload->attachments
);
}
}
/**
* @alias `$this->run()`
*
* @param Email_Message $payload
*
* @return void
*@throws \Exception
*
*/
public function send( Email_Message $payload ) {
$this->run( $payload );
}
/**
* Validate the email message DTO.
*
* @param Email_Message $payload
*
* @throws \ElementorPro\Core\Integrations\Exceptions\Action_Validation_Failed_Exception
*
* @return void
*/
public function validate( $payload ) {
$required_fields = [
'from',
'to',
'subject',
'body',
'content_type',
];
foreach ( $required_fields as $field ) {
if ( empty( $payload->{$field} ) ) {
throw new Action_Validation_Failed_Exception(
static::class,
"`Email_Message::\${$field}` is required."
);
}
}
}
/**
* Calls `wp_mail()`. Used for testing.
*
* @param mixed ...$args
*
* @return void
*/
protected function send_mail( ...$args ) {
add_action( 'wp_mail_failed', [ $this, 'on_wp_mail_error' ] );
wp_mail( ...$args );
remove_action( 'wp_mail_failed', [ $this, 'on_wp_mail_error' ] );
}
/**
* Throw exception on `wp_mail()` error.
*
* @param \WP_Error $error
*
* @throws \ElementorPro\Core\Integrations\Exceptions\Action_Failed_Exception
*
* @return void
*/
public function on_wp_mail_error( \WP_Error $error ) {
throw new Action_Failed_Exception( static::class, '`wp_mail()` cannot send email', $error );
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace ElementorPro\Core\Integrations\Exceptions;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Action_Failed_Exception extends Exception_Base {
protected function format_message( $message ) {
return sprintf(
'Action `%s` failed to run: %s',
$this->action,
$message
);
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace ElementorPro\Core\Integrations\Exceptions;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Action_Validation_Failed_Exception extends Exception_Base {
protected function format_message( $message ) {
return sprintf(
'Action `%s` failed validation: %s',
$this->action,
$message
);
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace ElementorPro\Core\Integrations\Exceptions;
use ElementorPro\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
abstract class Exception_Base extends \Exception {
/**
* @var string
*/
protected $action;
/**
* @var array
*/
protected $meta = [];
/**
* Get a formatted message specific to the current exception type.
*
* @param string $message
*
* @return string
*/
abstract protected function format_message( $message );
/**
* Exception_Base constructor.
*
* @param string $action - Action name that failed (ideally the class name, e.g. Email::class).
* @param string $message - Message to show.
* @param array $meta - Exception meta data. Used for logging.
*
*/
public function __construct( $action, $message = '', $meta = [] ) {
$this->action = $action;
$this->meta = $meta;
$message = $this->format_message( $message );
parent::__construct( $message );
}
/**
* Log the exception to Elementor's log.
*
* @return void
*/
public function log() {
Plugin::elementor()->logger->get_logger()->error( $this->getMessage(), [ 'meta' => $this->meta ] );
}
/**
* Get the error format.
*
* @return string
*/
public function __toString() {
return sprintf(
'%s: %s',
__CLASS__,
$this->getMessage()
);
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace ElementorPro\Core\Integrations;
use ElementorPro\Core\Integrations\Actions\Action_Base;
use ElementorPro\Core\Integrations\Actions\Email\Email;
use ElementorPro\Core\Integrations\Actions\Email\Email_Message;
use ElementorPro\Core\Integrations\Exceptions\Action_Failed_Exception;
use ElementorPro\Core\Integrations\Exceptions\Action_Validation_Failed_Exception;
use ElementorPro\Core\Utils\Registrar;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Integrations_Manager {
/**
* Registered action types.
*
* @var Registrar
*/
protected $actions_registrar;
/**
* Integrations_Manager constructor.
*
* @return void
*/
public function __construct() {
$this->actions_registrar = new Registrar();
}
/**
* Get an action instance.
*
* @shortcut `Registrar->get()`.
*
* @return \ElementorPro\Core\Integrations\Actions\Action_Base|null
*/
public function get_action( $id ) {
if ( ! $this->is_initialized() ) {
$this->init_actions();
}
return $this->actions_registrar->get( $id );
}
/**
* Run an action for a selected payload.
*
* @param array|mixed $payloads - Payloads instances to run the actions on.
* @param null|string $id - If `$payloads` is not an array, a custom action ID can be provided.
*
* @return void
*/
public function run( $payloads, $id = null ) {
if ( ! is_array( $payloads ) ) {
$payloads = $id ? [ $id => $payloads ] : [ $payloads ];
}
foreach ( $payloads as $key => $payload ) {
// Get the action ID for the provided payload type.
$action_id = is_numeric( $key ) ? get_class( $payload ) : $key;
/**
* @type Action_Base $action
*/
$action = $this->get_action( $action_id );
if ( ! $action ) {
throw new \Exception( "{$action_id} doesn't have an associated `Action`." );
}
if ( ! ( $action instanceof Action_Base ) ) {
$action_class = get_class( $action );
throw new \Exception( "{$action_class} is not a valid `Action_Base`." );
}
try {
$action->run( $payload );
} catch ( Action_Validation_Failed_Exception $e ) {
$e->log();
} catch ( Action_Failed_Exception $e ) {
$e->log();
}
}
}
/**
* Initialize the manager actions.
*
* @return void
*/
protected function init_actions() {
add_action( 'elementor_pro/core/integrations/actions/register', function ( Registrar $actions_registrar ) {
$actions_registrar->register( new Email(), Email_Message::class );
} );
do_action( 'elementor_pro/core/integrations/actions/register', $this->actions_registrar );
}
/**
* Determine if the manager is initialized.
*
* @return boolean
*/
protected function is_initialized() {
return ! ! did_action( 'elementor_pro/core/integrations/actions/register' );
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace ElementorPro\Core\Isolation;
interface Wordpress_Adapter_Interface {
public function has_post_thumbnail();
public function get_comments_number();
public function is_author( $author = ''): bool;
}

View File

@@ -0,0 +1,18 @@
<?php
namespace ElementorPro\Core\Isolation;
class Wordpress_Adapter implements Wordpress_Adapter_Interface {
public function has_post_thumbnail(): bool {
return has_post_thumbnail();
}
public function get_comments_number() {
return get_comments_number();
}
public function is_author( $author = '' ): bool {
return is_author( $author );
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace ElementorPro\Core;
use ElementorPro\Plugin;
use ElementorPro\Base\Module_Base;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
final class Modules_Manager {
/**
* @var Module_Base[]
*/
private $modules = [];
public function __construct() {
$modules = [
'query-control',
'custom-attributes',
'custom-css',
'page-transitions',
// role-manager Must be before Global Widget
'role-manager',
'global-widget',
'assets-manager',
'popup',
'motion-fx',
'usage',
'screenshots',
'compatibility-tag',
'admin-top-bar',
'notes',
'announcements',
'display-conditions',
'element-manager',
// Modules with Widgets.
'theme-builder',
'loop-builder',
'posts',
'gallery',
'forms',
'slides',
'nav-menu',
'animated-headline',
'hotspot',
'pricing',
'flip-box',
'call-to-action',
'carousel',
'table-of-contents',
'countdown',
'share-buttons',
'theme-elements',
'blockquote',
'custom-code',
'woocommerce',
'social',
'library',
'dynamic-tags',
'scroll-snap',
'sticky',
'wp-cli',
'lottie',
'code-highlight',
'video-playlist',
'payments',
'progress-tracker',
'mega-menu',
'nested-carousel',
'loop-filter',
'tiers',
];
foreach ( $modules as $module_name ) {
$class_name = str_replace( '-', ' ', $module_name );
$class_name = str_replace( ' ', '', ucwords( $class_name ) );
$class_name = '\ElementorPro\Modules\\' . $class_name . '\Module';
/** @var Module_Base $class_name */
$experimental_data = $class_name::get_experimental_data();
if ( $experimental_data ) {
Plugin::elementor()->experiments->add_feature( $experimental_data );
if ( ! Plugin::elementor()->experiments->is_feature_active( $experimental_data['name'] ) ) {
continue;
}
}
if ( $class_name::is_active() ) {
$this->modules[ $module_name ] = $class_name::instance();
}
}
}
/**
* @param string $module_name
*
* @return Module_Base|Module_Base[]
*/
public function get_modules( $module_name ) {
if ( $module_name ) {
if ( isset( $this->modules[ $module_name ] ) ) {
return $this->modules[ $module_name ];
}
return null;
}
return $this->modules;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace ElementorPro\Core\Notifications;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
abstract class Notification {
/**
* Get the payloads of the notification data shape (e.g. `Email_Message`, `Database_Message`). Those will automatically
* be sent over to the appropriate `Actions` under the `Integration_Manager` (using the `notify()` method).
* This method is also used to determine notification channels based on user ($notifiable) preferences.
*
* Returned shape:
* [
* $payload1_instance,
* $payload2_instance,
* ]
*
* @param \ElementorPro\Core\Notifications\Traits\Notifiable $notifiable - The notified model.
*
* @return array
*/
public function get_payloads( $notifiable ) {
return [];
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace ElementorPro\Core\Notifications;
use ElementorPro\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Notifications_Manager {
/**
* Send a notification.
*
* @param \ElementorPro\Core\Notifications\Notification $notification
* @param $notifiable
*
* @throws \Exception
*
* @return $this
*/
public function send( Notification $notification, $notifiable ) {
$payloads = $notification->get_payloads( $notifiable );
Plugin::instance()->integrations->run( $payloads );
return $this;
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace ElementorPro\Core\Notifications\Traits;
use ElementorPro\Core\Notifications\Notification;
use ElementorPro\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
trait Notifiable {
/**
* Notify a Model with a notification.
* Syntactic sugar for sending notifications via the `Notifications_Manager`.
*
* Usage:
* $model->notify( new User_Created_Notification( $new_user ) );
*
* @param Notification $notification - Notification to send.
*
* @throws \Exception
*
* @return void
*/
public function notify( Notification $notification ) {
Plugin::instance()->notifications->send( $notification, $this );
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace ElementorPro\Core;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* This class is responsible for the interaction with PHP Core API.
* The main benefit is making it easy to mock in testing.
*/
class PHP_Api {
/**
* @param $from
* @param $to
*
* @return bool
*/
public function move_uploaded_file( $from, $to ) {
return @ move_uploaded_file( $from, $to );
}
}

Some files were not shown because too many files have changed in this diff Show More