first commit

This commit is contained in:
2024-07-15 11:28:08 +02:00
commit f52d538ea5
21891 changed files with 6161164 additions and 0 deletions

View File

@@ -0,0 +1,260 @@
<?php
namespace Elementor\Core\Common;
use Elementor\Core\Base\App as BaseApp;
use Elementor\Core\Common\Modules\Ajax\Module as Ajax;
use Elementor\Core\Common\Modules\Finder\Module as Finder;
use Elementor\Core\Common\Modules\Connect\Module as Connect;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* App
*
* Elementor's common app that groups shared functionality, components and configuration
*
* @since 2.3.0
*/
class App extends BaseApp {
private $templates = [];
/**
* App constructor.
*
* @since 2.3.0
* @access public
*/
public function __construct() {
$this->add_default_templates();
add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'register_scripts' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'register_scripts' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts' ] );
add_action( 'elementor/editor/before_enqueue_styles', [ $this, 'register_styles' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'register_styles' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'register_styles' ], 9 );
add_action( 'elementor/editor/footer', [ $this, 'print_templates' ] );
add_action( 'admin_footer', [ $this, 'print_templates' ] );
add_action( 'wp_footer', [ $this, 'print_templates' ] );
}
/**
* Init components
*
* Initializing common components.
*
* @since 2.3.0
* @access public
*/
public function init_components() {
$this->add_component( 'ajax', new Ajax() );
if ( current_user_can( 'manage_options' ) ) {
if ( ! is_customize_preview() ) {
$this->add_component( 'finder', new Finder() );
}
}
$this->add_component( 'connect', new Connect() );
}
/**
* Get name.
*
* Retrieve the app name.
*
* @since 2.3.0
* @access public
*
* @return string Common app name.
*/
public function get_name() {
return 'common';
}
/**
* Register scripts.
*
* Register common scripts.
*
* @since 2.3.0
* @access public
*/
public function register_scripts() {
wp_register_script(
'elementor-common-modules',
$this->get_js_assets_url( 'common-modules' ),
[],
ELEMENTOR_VERSION,
true
);
wp_register_script(
'backbone-marionette',
$this->get_js_assets_url( 'backbone.marionette', 'assets/lib/backbone/' ),
[
'backbone',
],
'2.4.5.e1',
true
);
wp_register_script(
'backbone-radio',
$this->get_js_assets_url( 'backbone.radio', 'assets/lib/backbone/' ),
[
'backbone',
],
'1.0.4',
true
);
wp_register_script(
'elementor-dialog',
$this->get_js_assets_url( 'dialog', 'assets/lib/dialog/' ),
[
'jquery-ui-position',
],
'4.8.1',
true
);
wp_enqueue_script(
'elementor-common',
$this->get_js_assets_url( 'common' ),
[
'jquery',
'jquery-ui-draggable',
'backbone-marionette',
'backbone-radio',
'elementor-common-modules',
'elementor-dialog',
'wp-api-request',
],
ELEMENTOR_VERSION,
true
);
wp_set_script_translations( 'elementor-common', 'elementor' );
$this->print_config();
// Used for external plugins.
do_action( 'elementor/common/after_register_scripts', $this );
}
/**
* Register styles.
*
* Register common styles.
*
* @since 2.3.0
* @access public
*/
public function register_styles() {
wp_register_style(
'elementor-icons',
$this->get_css_assets_url( 'elementor-icons', 'assets/lib/eicons/css/' ),
[],
'5.11.0'
);
wp_enqueue_style(
'elementor-common',
$this->get_css_assets_url( 'common', null, 'default', true ),
[
'elementor-icons',
],
ELEMENTOR_VERSION
);
}
/**
* Add template.
*
* @since 2.3.0
* @access public
*
* @param string $template Can be either a link to template file or template
* HTML content.
* @param string $type Optional. Whether to handle the template as path
* or text. Default is `path`.
*/
public function add_template( $template, $type = 'path' ) {
if ( 'path' === $type ) {
ob_start();
include $template;
$template = ob_get_clean();
}
$this->templates[] = $template;
}
/**
* Print Templates
*
* Prints all registered templates.
*
* @since 2.3.0
* @access public
*/
public function print_templates() {
foreach ( $this->templates as $template ) {
echo $template;
}
}
/**
* Get init settings.
*
* Define the default/initial settings of the common app.
*
* @since 2.3.0
* @access protected
*
* @return array
*/
protected function get_init_settings() {
$active_experimental_features = Plugin::$instance->experiments->get_active_features();
$active_experimental_features = array_fill_keys( array_keys( $active_experimental_features ), true );
return [
'version' => ELEMENTOR_VERSION,
'isRTL' => is_rtl(),
'isDebug' => ( defined( 'WP_DEBUG' ) && WP_DEBUG ),
'isElementorDebug' => ( defined( 'ELEMENTOR_DEBUG' ) && ELEMENTOR_DEBUG ),
'activeModules' => array_keys( $this->get_components() ),
'experimentalFeatures' => $active_experimental_features,
'urls' => [
'assets' => ELEMENTOR_ASSETS_URL,
'rest' => get_rest_url(),
],
];
}
/**
* Add default templates.
*
* Register common app default templates.
* @since 2.3.0
* @access private
*/
private function add_default_templates() {
$default_templates = [
'includes/editor-templates/library-layout.php',
];
foreach ( $default_templates as $template ) {
$this->add_template( ELEMENTOR_PATH . $template );
}
}
}

View File

@@ -0,0 +1,27 @@
import ComponentBase from 'elementor-api/modules/component-base';
export default class BackwardsCompatibility {
ensureTab( namespace, tabSlug, page = '' ) {
let component = $e.components.get( namespace );
if ( ! component ) {
const Component = class extends ComponentBase {
getNamespace() {
return namespace;
}
renderTab( tab ) {
elementor.getPanelView().setPage( page ).activateTab( tab );
}
};
component = $e.components.register( new Component() );
}
if ( ! component.hasTab( tabSlug ) && elementor.config.tabs[ tabSlug ] ) {
component.addTab( tabSlug, {
title: elementor.config.tabs[ tabSlug ],
} );
}
}
}

View File

@@ -0,0 +1,26 @@
export default class CommandsBackwardsCompatibility extends elementorModules.Module {
__construct() {
this.onOrig = this.on;
}
on = ( eventName, callback ) => {
if ( 'run' === eventName ) {
let componentName = this.getConstructorID();
// Regex takes the first letter and convert it to lower case.
componentName = componentName.replace( /^./, ( val ) => val.toLowerCase() );
elementorCommon.helpers.softDeprecated(
`$e.${ componentName }.on( 'run', ... )`,
'3.0.0',
`$e.${ componentName }.on( 'run:before', ... )`
);
this.onOrig( 'run:before', callback );
return;
}
this.onOrig( eventName, callback );
};
}

View File

@@ -0,0 +1,7 @@
import Commands from './commands.js';
export default class CommandsInternal extends Commands {
error( message ) {
throw Error( 'Commands internal: ' + message );
}
}

View File

@@ -0,0 +1,351 @@
import CommandsBackwardsCompatibility from './backwards-compatibility/commands';
export default class Commands extends CommandsBackwardsCompatibility {
/**
* Function constructor().
*
* Create `$e.commands` API.
*
* @param {{}} args
*/
constructor( ...args ) {
super( ...args );
this.current = {};
this.currentArgs = {};
this.currentTrace = [];
this.commands = {};
this.components = {};
this.classes = {};
}
/**
* @param id
* @returns {CommandBase}
*/
getCommandClass( id ) {
return this.classes[ id ];
}
/**
* Function getAll().
*
* Receive all loaded commands.
*
* @returns {string[]}
*/
getAll() {
return Object.keys( this.commands ).sort();
}
/**
* Function register().
*
* Register new command.
*
* @param {ComponentBase|string} component
* @param {string} command
* @param {function()} callback
*
* @returns {Commands}
*/
register( component, command, callback ) {
let namespace;
if ( 'string' === typeof component ) {
namespace = component;
component = $e.components.get( namespace );
if ( ! component ) {
this.error( `'${ namespace }' component is not exist.` );
}
} else {
namespace = component.getNamespace();
}
const fullCommand = namespace + ( command ? '/' + command : '' );
if ( this.commands[ fullCommand ] ) {
this.error( `\`${ fullCommand }\` is already registered.` );
}
this.commands[ fullCommand ] = callback;
this.components[ fullCommand ] = namespace;
const shortcuts = component.getShortcuts(),
shortcut = shortcuts[ command ];
if ( shortcut ) {
shortcut.command = fullCommand;
shortcut.callback = ( event ) => this.runShortcut( fullCommand, event );
$e.shortcuts.register( shortcut.keys, shortcut );
}
return this;
}
unregister( component, command ) {
let namespace;
if ( 'string' === typeof component ) {
namespace = component;
component = $e.components.get( namespace );
if ( ! component ) {
this.error( `'${ namespace }' component is not exist.` );
}
} else {
namespace = component.getNamespace();
}
const fullCommand = namespace + ( command ? '/' + command : '' );
if ( ! this.commands[ fullCommand ] ) {
this.error( `\`${ fullCommand }\` not exist.` );
}
delete this.commands[ fullCommand ];
delete this.components[ fullCommand ];
const shortcuts = component.getShortcuts(),
shortcut = shortcuts[ command ];
if ( shortcut ) {
$e.shortcuts.unregister( shortcut.keys, shortcut );
}
return this;
}
/**
* Function getComponent().
*
* Receive Component of the command.
*
* @param {string} command
*
* @returns {Component}
*/
getComponent( command ) {
const namespace = this.components[ command ];
return $e.components.get( namespace );
}
/**
* Function is().
*
* Checks if current running command is the same parameter command.
*
* @param {string} command
*
* @returns {boolean}
*/
is( command ) {
const component = this.getComponent( command );
if ( ! component ) {
return false;
}
return command === this.current[ component.getRootContainer() ];
}
/**
* Function isCurrentFirstTrace().
*
* Checks if parameter command is the first command in trace that currently running.
*
* @param {string} command
*
* @returns {boolean}
*/
isCurrentFirstTrace( command ) {
return command === this.getCurrentFirstTrace();
}
/**
* Function getCurrent().
*
* Receive currently running components and its commands.
*
* @param {string} container
*
* @returns {{}|boolean|*}
*/
getCurrent( container = '' ) {
if ( container ) {
if ( ! this.current[ container ] ) {
return false;
}
return this.current[ container ];
}
return this.current;
}
/**
* Function getCurrentArgs().
*
* Receive currently running command args.
*
* @param {string} container
*
* @returns {{}|boolean|*}
*/
getCurrentArgs( container = '' ) {
if ( container ) {
if ( ! this.currentArgs[ container ] ) {
return false;
}
return this.currentArgs[ container ];
}
return this.currentArgs;
}
/**
* Function getCurrentFirst().
*
* Receive first command that currently running.
*
* @returns {string}
*/
getCurrentFirst() {
return Object.values( this.current )[ 0 ];
}
/**
* Function getCurrentLast().
*
* Receive last command that currently running.
*
* @returns {string}
*/
getCurrentLast() {
const current = Object.values( this.current );
return current[ current.length - 1 ];
}
/**
* Function getCurrentFirstTrace().
*
* Receive first command in trace that currently running
*
* @returns {string}
*/
getCurrentFirstTrace() {
return this.currentTrace[ 0 ];
}
/**
* Function beforeRun().
*
* @param {string} command
* @param {} args
*
* @returns {boolean} dependency result
*/
beforeRun( command, args = {} ) {
if ( ! this.commands[ command ] ) {
this.error( `\`${ command }\` not found.` );
}
this.currentTrace.push( command );
return this.getComponent( command ).dependency( command, args );
}
/**
* Function run().
*
* Runs a command.
*
* @param {string} command
* @param {{}} args
*
* @returns {boolean|*} results
*/
run( command, args = {} ) {
if ( ! this.beforeRun( command, args ) ) {
return false;
}
const component = this.getComponent( command ),
container = component.getRootContainer();
this.current[ container ] = command;
this.currentArgs[ container ] = args;
this.trigger( 'run:before', component, command, args );
if ( args.onBefore ) {
args.onBefore.apply( component, [ args ] );
}
const results = this.commands[ command ].apply( component, [ args ] );
// TODO: Consider add results to `$e.devTools`.
if ( args.onAfter ) {
args.onAfter.apply( component, [ args, results ] );
}
this.trigger( 'run:after', component, command, args, results );
this.afterRun( command );
if ( false === args.returnValue ) {
return true;
}
return results;
}
/**
* Function runShortcut().
*
* Run shortcut.
*
* It's separated in order to allow override.
*
* @param {string} command
* @param {*} event
*
* @returns {boolean|*}
*/
runShortcut( command, event ) {
return this.run( command, event );
}
/**
* Function afterRun().
*
* Method fired after the command runs.
*
* @param {string} command
*/
afterRun( command ) {
const component = this.getComponent( command ),
container = component.getRootContainer();
this.currentTrace.pop();
delete this.current[ container ];
delete this.currentArgs[ container ];
}
/**
* Function error().
*
* Throws error.
*
* @throw {Error}
*
* @param {string} message
*/
error( message ) {
throw Error( `Commands: ${ message }` );
}
}

View File

@@ -0,0 +1,49 @@
export default class extends elementorModules.Module {
constructor( ...args ) {
super( ...args );
this.components = {};
this.activeComponents = {};
}
getAll() {
return Object.keys( this.components ).sort();
}
register( component ) {
if ( this.components[ component.getNamespace() ] ) {
return;
}
component.registerAPI();
this.components[ component.getNamespace() ] = component;
return component;
}
/**
* @returns {Component}
*/
get( id ) {
return this.components[ id ];
}
getActive() {
return this.activeComponents;
}
activate( namespace ) {
// Add as last.
this.inactivate( namespace );
this.activeComponents[ namespace ] = true;
}
inactivate( namespace ) {
delete this.activeComponents[ namespace ];
}
isActive( namespace ) {
return !! this.activeComponents[ namespace ];
}
}

View File

@@ -0,0 +1,534 @@
import ArgsObject from 'elementor-assets-js/modules/imports/args-object';
import Commands from './commands.js';
import Cache from './data/cache';
/**
* @typedef {('create'|'delete'|'get'|'update'|'options')} DataTypes
*/
/**
* @typedef {{}} RequestData
* @property {ComponentBase} component
* @property {string} command
* @property {string} endpoint
* @property {DataTypes} [type]
* @property {{}} [args]
* @property {number} [timestamp]
* @property {('hit'|'miss')} [cache]
*/
/**
* @typedef {object} ExtractedCommand
* @property {string} command
* @property {object} args
*/
// TODO: Return it from the server. Original at WP_REST_Server.
export const READABLE = [ 'GET' ],
CREATABLE = [ 'POST' ],
EDITABLE = [ 'POST', 'PUT', 'PATCH' ],
DELETABLE = [ 'DELETE' ],
ALLMETHODS = [ 'GET', 'POST', 'PUT', 'PATCH', 'DELETE' ];
export default class Data extends Commands {
constructor( args = {} ) {
super( args );
this.args = Object.assign( args, {
namespace: 'elementor',
version: '1',
} );
this.cache = new Cache( this );
this.validatedRequests = {};
this.commandFormats = {};
this.baseEndpointAddress = '';
const { namespace, version } = this.args;
this.baseEndpointAddress = `${ elementorCommon.config.urls.rest }${ namespace }/v${ version }/`;
}
/**
* Function getHTTPMethod().
*
* Returns HTTP Method by type.
*
* @param {DataTypes} type
*
* @return {string|boolean}
*/
getHTTPMethod( type ) {
switch ( type ) {
case 'create':
return 'POST';
case 'delete':
return 'DELETE';
case 'get':
return 'GET';
case 'update':
return 'PUT';
case 'options':
return 'OPTIONS';
}
return false;
}
/**
* Function getAllowedMethods().
*
* Returns allowed HTTP methods by type.
*
* @param {DataTypes} type
*
* @return {[string]|boolean}
*/
getAllowedMethods( type ) {
switch ( type ) {
case 'create':
return CREATABLE;
case 'delete':
return DELETABLE;
case 'get':
return READABLE;
case 'update':
return EDITABLE;
case 'options':
return [ 'OPTIONS' ];
}
return false;
}
/**
* Function commandToEndpoint().
*
* Convert command to endpoint.
*
* For example `component/command/{arg}` => `controller/endpoint/8`.
*
* TODO: Find a better solution.
*
* @param {string} command
* @param {{}} args
* @param {string|null} [format]
*
* @returns {string} endpoint
*/
commandToEndpoint( command, args, format = null ) {
let endpoint = command;
const argsQueryLength = args?.query ? Object.values( args.query ).length : 0;
if ( argsQueryLength && format && format.includes( '/{' ) ) {
// Means command includes magic query arguments ( controller/endpoint/{whatever} ).
const magicParams = format.split( '/' ).filter( ( str ) => '{' === str.charAt( 0 ) );
magicParams.forEach( ( param ) => {
// Remove the '{', '}'.
param = param.replace( '{', '' );
param = param.replace( '}', '' );
const formatted = Object.entries( args.query ).find( ( [ key ] ) => key === param );
if ( ! formatted ) {
return;
}
const key = formatted[ 0 ],
value = formatted[ 1 ].toString();
// Replace magic params with values.
format = format.replace( new RegExp( '{' + param + '}', 'g' ), value );
delete args.query[ key ];
} );
}
if ( format ) {
endpoint = format;
}
// If requested magic param does not exist in args, need to remove it to have fixed endpoint.
// eg: 'documents/{documentId}/elements/{elementId}' and args { documentId: 4123 }.
// result: 'documents/4123/elements'
if ( format && endpoint.includes( '/{' ) ) {
endpoint = endpoint.substring( 0, endpoint.indexOf( '/{' ) );
}
if ( args.query && Object.values( args.query ).length ) {
// Sorting since the endpoint later will be used as key to store the cache.
const queryEntries = Object.entries( args.query ).sort(
( [ aKey ], [ bKey ] ) => aKey - bKey // Sort by param name.
);
// `args.query` will become a part of GET params.
if ( queryEntries.length ) {
endpoint += '?';
queryEntries.forEach( ( [ name, value ] ) => {
// Replace the character '/' with the encoded version,
// mostly because when saving this endpoint value to the cache it splits the url base on the '/' character.
value = `${ value }`.replace( /\//g, '%2F' );
endpoint += name + '=' + value + '&';
} );
}
// If last character is '&' remove it.
endpoint = endpoint.replace( /&$/, '' );
}
return endpoint;
}
/**
* Function commandExtractArgs().
*
* If the command have query convert it to args.
*
* @param {string} command
* @param {object} args
*
* @returns {ExtractedCommand} command
*/
commandExtractArgs( command, args = {} ) {
if ( command?.includes( '?' ) ) {
if ( ! args.query ) {
args.query = {};
}
const commandParts = command.split( '?' ),
pureCommand = commandParts[ 0 ],
queryString = commandParts[ 1 ],
query = new URLSearchParams( queryString );
Object.assign( args.query, Object.fromEntries( query ) );
command = pureCommand;
}
return {
command,
args,
};
}
/**
* Function validateRequestData().
*
* Validate request data requirements.
*
* @param {RequestData} requestData
* @param {boolean} [requireArgsData]
*/
validateRequestData( requestData, requireArgsData = false ) {
// Do not validate if its already valid.
if ( requestData.timestamp && this.validatedRequests[ requestData.timestamp ] ) {
return;
}
const argsObject = new ArgsObject( requestData );
argsObject.requireArgument( 'component' );
argsObject.requireArgumentType( 'command', 'string' );
argsObject.requireArgumentType( 'endpoint', 'string' );
if ( requireArgsData ) {
argsObject.requireArgumentType( 'data', 'object', requestData.args );
}
// Ensure timestamp.
if ( ! requestData.timestamp ) {
requestData.timestamp = new Date().getTime();
}
this.validatedRequests[ requestData.timestamp ] = true;
}
/**
* Function prepareHeaders().
*
* @param {RequestData} requestData
*
* @return {{}} params
*/
prepareHeaders( requestData ) {
/* global wpApiSettings */
const type = requestData.type,
nonce = wpApiSettings.nonce,
params = {
signal: requestData.args?.options?.signal,
credentials: 'include', // cookies is required for wp reset.
},
headers = { 'X-WP-Nonce': nonce };
/**
* Translate:
* 'create, delete, get, update' to HTTP Methods:
* 'GET, POST, PUT, PATCH, DELETE'
*/
const allowedMethods = this.getAllowedMethods( type ),
method = this.getHTTPMethod( type );
if ( 'GET' === method ) {
Object.assign( params, { headers } );
} else if ( allowedMethods ) {
if ( [ 'POST', 'PUT' ].includes( method ) && ! requestData.args?.data ) {
throw Error( 'Invalid requestData.args.data' );
}
Object.assign( headers, { 'Content-Type': 'application/json' } );
Object.assign( params, {
method,
headers,
body: JSON.stringify( requestData.args.data ),
} );
} else {
throw Error( `Invalid type: '${ type }'` );
}
return params;
}
/**
* This method response for building a final endpoint,
* the main problem is with plain permalink mode + command with query params that creates a weird url,
* the current method should fix it.
*
* @param endpoint
* @returns {string}
*/
prepareEndpoint( endpoint ) {
const splitUrl = endpoint.split( '?' ),
path = splitUrl.shift();
let url = this.baseEndpointAddress + path;
if ( splitUrl.length ) {
const separator = url.includes( '?' ) ? '&' : '?';
url += separator + splitUrl.pop();
}
return url;
}
/**
* Function fetch().
*
* @param {RequestData} requestData
* @param {function(input: RequestInfo, init?) : Promise<Response> } [fetchAPI]
*
* @return {{}} params
*/
fetch( requestData, fetchAPI = window.fetch ) {
requestData.cache = 'miss';
const params = this.prepareHeaders( requestData ),
refresh = requestData.args.options?.refresh,
getCache = 'get' === requestData.type && ! refresh,
saveCache = [ 'create', 'get' ].includes( requestData.type ) && ! refresh;
if ( getCache ) {
const cachePromise = this.cache.getAsync( requestData );
if ( cachePromise ) {
return cachePromise;
}
}
return new Promise( async ( resolve, reject ) => {
// This function is async because:
// it needs to wait for the results, to cache them before it resolve's the promise.
try {
const endpoint = this.prepareEndpoint( requestData.endpoint ),
request = fetchAPI( endpoint, params ),
response = await request.then( async ( _response ) => {
if ( ! _response.ok ) {
// Catch WP REST errors.
if ( _response.headers.get( 'content-type' ).includes( 'application/json' ) ) {
_response = await _response.json();
}
throw _response;
}
return _response.json();
} );
// At this point, it got the resolved response from remote.
// So load cache, and resolve it.
if ( saveCache ) {
this.cache.set( requestData, response );
}
resolve( response );
} catch ( e ) {
reject( e );
}
} );
}
/**
* Function getCache().
*
* @param {ComponentBase} component
* @param {string} command
* @param {{}} query
*
* @returns {{}}
*/
getCache( component, command, query = {} ) {
const args = { query };
return this.cache.get( {
endpoint: this.commandToEndpoint( command, args, this.commandFormats[ command ] ),
component,
command,
args,
} );
}
/**
* Function setCache().
*
* @param {ComponentBase} component
* @param {string} command
* @param {{}} query
* @param {*} data
*/
setCache( component, command, query, data ) {
const args = { query };
this.cache.set( {
endpoint: this.commandToEndpoint( command, args, this.commandFormats[ command ] ),
component,
command,
args,
},
data
);
}
/**
* Function updateCache().
*
* The difference between 'setCache' and 'updateCache' is update will only modify exist values.
* and 'setCache' will create or update.
*
* @param {ComponentBase} component
* @param {string} command
* @param {{}} query
* @param {*} data
*/
updateCache( component, command, query, data ) {
const args = { query, data };
this.cache.update( {
endpoint: this.commandToEndpoint( command, args, this.commandFormats[ command ] ),
component,
command,
args,
} );
}
/**
* Function deleteCache().
*
* @param {ComponentBase} component
* @param {string} command
* @param {{}} query
*/
deleteCache( component, command, query = {} ) {
const args = { query };
this.cache.delete( {
endpoint: this.commandToEndpoint( command, args, this.commandFormats[ command ] ),
component,
command,
args,
}
);
}
/**
* Function registerFormat().
*
* Register's format for each command.
*
* @param {string} command
* @param {string} format
*/
registerFormat( command, format ) {
this.commandFormats[ command ] = format;
}
create( command, data, query = {}, options = {} ) {
return this.run( 'create', command, { query, options, data } );
}
delete( command, query = {}, options = {} ) {
return this.run( 'delete', command, { query, options } );
}
get( command, query = {}, options = {} ) {
return this.run( 'get', command, { query, options } );
}
update( command, data, query = {}, options = {} ) {
return this.run( 'update', command, { query, options, data } );
}
options( command, query, options = {} ) {
return this.run( 'options', command, { query, options } );
}
/**
* @param {ComponentBase} component
* @param {string} command
* @param callback
*/
register( component, command, callback ) {
super.register( component, command, callback );
const fullCommandName = component.getNamespace() + '/' + command,
commandInstance = $e.commands.getCommandClass( fullCommandName ),
format = commandInstance?.getEndpointFormat ? commandInstance.getEndpointFormat() : false;
if ( format ) {
$e.data.registerFormat( fullCommandName, format );
}
}
/**
* TODO: Add JSDOC typedef for args ( query and options ).
*
* @param {DataTypes} type
* @param {string} command
* @param {{}} args
*
* @return {*}
*/
run( type, command, args ) {
args.options.type = type;
( { command, args } = this.commandExtractArgs( command, args ) );
return super.run( command, args );
}
error( message ) {
throw Error( 'Data commands: ' + message );
}
}

View File

@@ -0,0 +1,248 @@
import LocalStorage from './storages/local-storage';
/**
* TODO: Search common logic, create functions to reduce code size.
*/
export default class Cache {
/**
* Function constructor().
*
* Create cache.
*
* @param {Data} manager
*/
constructor( manager ) {
this.manager = manager;
this.storage = new LocalStorage();
}
/**
* Function getAsync().
*
* Receive from cache. the difference between getAsync() and get() is that receive return it as promise...
* to fake fetch mechanism.
*
* @param {RequestData} requestData
*
* @return {(Promise|boolean)}
*/
getAsync( requestData ) {
const data = this.get( requestData );
if ( null !== data ) {
// If data comes from cache, add 'cache = hit' to requestData.
requestData.cache = 'hit';
return new Promise( async ( resolve ) => {
resolve( data );
} );
}
// TODO: Check if possible, always return promise and reject it.
return false;
}
/**
* Function set().
*
* set data to cache.
*
* The difference between set() and update() is that set, will modify the data anyway...
* when update() will only modify exist objects/values.
*
* @param {RequestData} requestData
* @param {*} data
*/
set( requestData, data ) {
$e.data.validateRequestData( requestData );
const componentName = requestData.component.getNamespace(),
pureEndpoint = requestData.endpoint.replace( componentName + '/', '' ),
pureEndpointParts = pureEndpoint.split( '/' );
let newData = {};
// Example of working with reaming endpoint part(s) can be found at 'cache.spec.js' test: 'load(): deep'.
// Analyze reaming endpoint.
if ( pureEndpointParts.length && pureEndpoint !== componentName ) {
// Using reaming endpoint parts, to build new data object.
const result = pureEndpointParts.reduce( ( accumulator, pureEndpointPart ) => {
accumulator[ pureEndpointPart ] = {};
return accumulator[ pureEndpointPart ];
}, newData );
// 'result' is equal to 'newData' with a deeper pointer, build based on 'pureEndpointParts' ( will effect newData ).
Object.assign( result, data );
} else {
newData = data;
}
const oldData = this.storage.getItem( componentName );
// When have old data, merge it recursively with newData using jQuery.extend().
if ( oldData !== null ) {
newData = jQuery.extend( true, oldData, newData );
}
this.storage.setItem( componentName, newData );
}
/**
* Function get().
*
* Get from exist storage.
*
* @param {RequestData} requestData
*
* @return {{}}
*/
get( requestData ) {
$e.data.validateRequestData( requestData );
const componentName = requestData.component.getNamespace(),
componentData = this.storage.getItem( componentName );
if ( componentData !== null ) {
if ( componentName === requestData.endpoint ) {
return componentData;
}
// Example of working with reaming endpoint part(s) can be found at 'cache.spec.js' test: 'get(): complex'.
// Analyze reaming endpoint (Using reduce over endpoint parts, build the right index).
const pureEndpoint = requestData.endpoint.replace( requestData.component.getNamespace() + '/', '' ),
pureEndpointParts = pureEndpoint.split( '/' ),
result = pureEndpointParts.reduce( ( accumulator, endpointPart ) => {
if ( accumulator && accumulator[ endpointPart ] ) {
return accumulator[ endpointPart ];
}
}, componentData );
// Since $e.data.cache.receive will reject only if null is the result.
return result || null;
}
return null;
}
/**
* Function update().
*
* Update only already exist storage, runs over all storage
*
* @param {RequestData} requestData
*
* @return {boolean} is updated
*/
update( requestData ) {
$e.data.validateRequestData( requestData, true );
const endpoint = requestData.endpoint;
let response = {};
// Simulate response from cache.
Object.entries( this.storage.getAll() ).forEach( ( [ endpointKey, /*string*/ endpointValue ] ) => {
if ( endpointValue && endpoint.includes( endpointKey ) ) {
// Assuming it is a specific endpoint.
const oldData = endpointValue,
pureEndpoint = requestData.endpoint.replace( requestData.component.getNamespace() + '/', '' ),
pureEndpointParts = pureEndpoint.split( '/' ),
isComponentUpdate = 1 === pureEndpointParts.length && endpointKey === requestData.endpoint && endpointKey === requestData.component.getNamespace();
// Component update or specific update?
if ( isComponentUpdate ) {
response = jQuery.extend( true, oldData, requestData.args.data );
} else {
const oldSpecificData = pureEndpointParts.reduce(
( accumulator, pureEndpointPart ) => accumulator[ pureEndpointPart ], oldData
);
response = jQuery.extend( true, oldSpecificData, requestData.args.data );
}
}
} );
// If response not found.
if ( 0 === Object.values( response ).length ) {
return false;
}
// Update cache.
this.set( requestData, response );
return true;
}
/**
* Function delete().
*
* Delete endpoint from storage.
*
* @param {RequestData} requestData
*
* @return {boolean} is deleted
*/
delete( requestData ) {
$e.data.validateRequestData( requestData );
let result = false;
const componentName = requestData.component.getNamespace();
if ( componentName !== requestData.endpoint ) {
const oldData = this.storage.getItem( componentName ),
newData = {};
if ( null === oldData ) {
return false;
}
const pureEndpoint = requestData.endpoint.replace( componentName + '/', '' ),
pureEndpointParts = pureEndpoint.split( '/' ),
lastEndpointPart = pureEndpointParts[ pureEndpointParts.length - 1 ];
pureEndpointParts.reduce( ( accumulator, pureEndpointPart ) => {
if ( pureEndpointPart === lastEndpointPart ) {
// Null, means delete.
accumulator[ pureEndpointPart ] = null;
} else {
accumulator[ pureEndpointPart ] = {};
}
return accumulator[ pureEndpointPart ];
}, newData );
if ( Object.keys( oldData ).length ) {
const deleteKeys = ( target, nullsObject ) => {
if ( nullsObject ) {
Object.keys( nullsObject ).forEach( ( key ) => {
if ( nullsObject[ key ] && 'object' === typeof nullsObject[ key ] ) {
deleteKeys( target[ key ], nullsObject[ key ] );
} else if ( null === nullsObject[ key ] ) {
delete target[ key ];
result = true;
}
} );
} else {
// Means need to clear all the object.
Object.keys( target ).forEach( ( key ) => delete target[ key ] );
}
return target;
};
this.storage.setItem( componentName, deleteKeys( oldData, newData ) );
}
} else {
for ( const key in this.storage.getAll() ) {
if ( key === requestData.endpoint ) {
this.storage.removeItem( requestData.endpoint );
result = true;
break;
}
}
}
return result;
}
}

View File

@@ -0,0 +1,36 @@
import BaseStorage from 'elementor-api/core/data/storages/base-storage';
export default class BasePrefixStorage extends BaseStorage {
static DEFAULT_KEY_PREFIX = 'e_';
clear() {
Object.keys( this.getAll() ).forEach( ( key ) => this.removeItem( key ) );
}
getItem( key ) {
return super.getItem( BasePrefixStorage.DEFAULT_KEY_PREFIX + key );
}
removeItem( key ) {
return super.removeItem( BasePrefixStorage.DEFAULT_KEY_PREFIX + key );
}
setItem( key, value ) {
return super.setItem( BasePrefixStorage.DEFAULT_KEY_PREFIX + key, value );
}
getAll() {
const { DEFAULT_KEY_PREFIX } = BasePrefixStorage,
keys = Object.keys( this.provider ),
result = {};
keys.forEach( ( key ) => {
if ( key.startsWith( DEFAULT_KEY_PREFIX ) ) {
key = key.replace( DEFAULT_KEY_PREFIX, '' );
result[ key ] = this.getItem( key );
}
} );
return result;
}
}

View File

@@ -0,0 +1,57 @@
/**
* TODO: Merge all storage's to one.
* Using this technique give's the ability to use JSDOC from 'window.storage'.
*
* @implements {Storage}
*/
export default class BaseStorage {
/**
* Create storage wrapper.
*
* @param {Storage} provider
*/
constructor( provider ) {
if ( BaseStorage === new.target ) {
throw new TypeError( 'Cannot construct BaseStorage instances directly' );
}
this.provider = provider;
}
clear() {
return this.provider.clear();
}
getItem( key ) {
const result = this.provider.getItem( key );
if ( null !== result ) {
return JSON.parse( result );
}
return result;
}
key( index ) {
return this.provider.key( index );
}
removeItem( key ) {
return this.provider.removeItem( key );
}
setItem( key, value ) {
return this.provider.setItem( key, JSON.stringify( value ) );
}
getAll() {
const keys = Object.keys( this.provider ),
result = {};
keys.forEach( ( key ) => {
result[ key ] = this.getItem( key );
} );
return result;
}
}

View File

@@ -0,0 +1,20 @@
import BasePrefixStorage from './base-prefix-storage';
export default class LocalStorage extends BasePrefixStorage {
constructor() {
super( localStorage );
}
debug() {
const entries = this.getAll(),
ordered = {};
Object.keys( entries ).sort().forEach( ( key ) => {
const value = entries[ key ];
ordered[ key ] = value;
} );
return ordered;
}
}

View File

@@ -0,0 +1,250 @@
import HooksData from './hooks/data.js';
import HooksUI from './hooks/ui.js';
export default class Hooks {
data = new HooksData();
ui = new HooksUI();
/**
* Function activate().
*
* Activate all hooks.
*/
activate() {
this.getTypes().forEach( ( hooksType ) => {
hooksType.activate();
} );
}
/**
* Function deactivate().
*
* Deactivate all hooks.
*/
deactivate() {
this.getTypes().forEach( ( hooksType ) => {
hooksType.deactivate();
} );
}
getAll( flat = false ) {
const result = {};
this.getTypes().forEach( ( hooksType ) => {
result[ hooksType.getType() ] = hooksType.getAll( flat );
} );
return result;
}
getTypes() {
return [
this.data,
this.ui,
];
}
getType( type ) {
return this.getTypes().find(
( hooks ) => type === hooks.getType()
);
}
/**
* Function register().
*
* Register hook.
*
* @param {string} type
* @param {string} event
* @param {HookBase} instance
*
* @returns {{}} Created callback
*/
register( type, event, instance ) {
return this.getType( type ).register( event, instance );
}
/**
* Function run().
*
* Run's a hook.
*
* @param {string} type
* @param {string} event
* @param {string} command
* @param {{}} args
* @param {*} result
*
* @returns {boolean}
*/
run( type, event, command, args, result = undefined ) {
return this.getType( type ).run( event, command, args, result );
}
/**
* Function registerDataAfter().
*
* Register data hook that's run after the command.
*
* @param {HookBase} instance
*
* @returns {{}}
*/
registerDataAfter( instance ) {
return this.register( 'data', 'after', instance );
}
/**
* Function registerDataCatch().
*
* Register data hook that's run when the command fails.
*
* @param {HookBase} instance
*
* @returns {{}}
*/
registerDataCatch( instance ) {
return this.register( 'data', 'catch', instance );
}
/**
* Function registerDataDependency().
*
* Register data hook that's run before the command as dependency.
*
* @param {HookBase} instance
*
* @returns {{}}
*/
registerDataDependency( instance ) {
return this.register( 'data', 'dependency', instance );
}
/**
* Function registerUIAfter().
*
* Register UI hook that's run after the commands run.
*
* @param {HookBase} instance
*
* @returns {{}}
*/
registerUIAfter( instance ) {
return this.register( 'ui', 'after', instance );
}
/**
* Function registerUICatch().
*
* Register UI hook that's run when the command fails.
*
* @param {HookBase} instance
*
* @returns {{}}
*/
registerUICatch( instance ) {
return this.register( 'ui', 'catch', instance );
}
/**
* Function registerUIBefore().
*
* Register UI hook that's run before the command.
*
* @param {HookBase} instance
*
* @returns {{}}
*/
registerUIBefore( instance ) {
return this.register( 'ui', 'before', instance );
}
/**
* Function runDataAfter().
*
* Run data hook that's run after the command.
*
* @param {string} command
* @param {{}} args
* @param {*} result
*
* @returns {boolean}
*/
runDataAfter( command, args, result ) {
return this.run( 'data', 'after', command, args, result );
}
/**
* Function runDataCatch().
*
* Run data hook that's run when the command fails.
*
* @param {string} command
* @param {{}} args
* @param {*} error
*
* @returns {boolean}
*/
runDataCatch( command, args, error ) {
return this.run( 'data', 'catch', command, args, error );
}
/**
* Function runDataDependency().
*
* Run data hook that's run before the command as dependency.
*
* @param {string} command
* @param {{}} args
*
* @returns {boolean}
*/
runDataDependency( command, args ) {
return this.run( 'data', 'dependency', command, args );
}
/**
* Function runUIAfter().
*
* Run UI hook that's run after the commands run.
*
* @param {string} command
* @param {{}} args
* @param {*} result
*
* @returns {boolean}
*/
runUIAfter( command, args, result ) {
return this.run( 'ui', 'after', command, args, result );
}
/**
* Function runUICatch().
*
* Run UI hook that's run when the command fails.
*
* @param {string} command
* @param {{}} args
* @param {*} e
*
* @returns {boolean}
*/
runUICatch( command, args, e ) {
return this.run( 'ui', 'catch', command, args, e );
}
/**
* Function runUIBefore().
*
* Run UI hook that's run before the command.
*
* @param {string} command
* @param {{}} args
*
* @returns {boolean}
*/
runUIBefore( command, args ) {
return this.run( 'ui', 'before', command, args );
}
}

View File

@@ -0,0 +1,428 @@
export default class HooksBase extends elementorModules.Module {
/**
* Function constructor().
*
* Create hooks base.
*
* @param {{}} args
*/
constructor( ...args ) {
super( ...args );
/**
* Current command.
*
* @type {string}
*/
this.current = '';
/**
* Array of ids which in use.
*
* @type {Array}
*/
this.usedIds = [];
/**
* Object of callbacks that was bound by container type.
*
* @type {{}}
*/
this.callbacks = {
after: {},
catch: {},
};
/**
* Object of depth.
*
* @type {{}}
*/
this.depth = {
after: {},
catch: {},
};
this.callbacksFlatList = {};
}
activate() {
Object.values( this.getAll( true ) ).forEach( ( callback ) => {
callback.activate();
} );
}
deactivate() {
Object.values( this.getAll( true ) ).forEach( ( callback ) => {
callback.deactivate();
} );
}
/**
* Function getType().
*
* Returns type eg: ( event, hook, etc ... ).
*
* @returns {string} type
*/
getType() {
elementorModules.forceMethodImplementation();
}
get( id ) {
return this.callbacksFlatList[ id ];
}
/**
* Function getAll().
*
* Return all possible callbacks.
*
* @param {boolean} flat
*
* @returns {{}}
*/
getAll( flat = false ) {
if ( flat ) {
return this.callbacksFlatList;
}
const result = {};
Object.keys( this.callbacks ).forEach( ( event ) => {
if ( ! result[ event ] ) {
result[ event ] = [];
}
Object.keys( this.callbacks[ event ] ).forEach( ( command ) => {
result[ event ].push( {
command,
callbacks: this.callbacks[ event ][ command ],
} );
} );
} );
return result;
}
/**
* Function getCurrent();
*
* Return current command.
*
* @returns {string}
*/
getCurrent() {
return this.current;
}
/**
* Function getUsedIds().
*
* Returns the current used ids.
*
* @returns {Array}
*/
getUsedIds() {
return this.usedIds;
}
/**
* Function getCallbacks().
*
* Get available callbacks for specific event and command.
*
* @param {string} event
* @param {string} command
*
* @returns {(array|boolean)} callbacks
*/
getCallbacks( event, command, args ) {
const { containers = [ args.container ] } = args,
containerType = containers[ 0 ] ? containers[ 0 ].type : false;
let callbacks = [];
if ( this.callbacks[ event ] && this.callbacks[ event ][ command ] ) {
if ( containerType && this.callbacks[ event ][ command ][ containerType ] ) {
callbacks = callbacks.concat( this.callbacks[ event ][ command ][ containerType ] );
}
if ( this.callbacks[ event ][ command ].all ) {
callbacks = callbacks.concat( this.callbacks[ event ][ command ].all );
}
}
if ( callbacks.length ) {
return callbacks;
}
return false;
}
/**
* function checkEvent().
*
* Validate if the event is available.
*
* @param {string} event
*/
checkEvent( event ) {
if ( -1 === Object.keys( this.callbacks ).indexOf( event ) ) {
throw Error( `${ this.getType() }: '${ event }' is not available.` );
}
}
/**
* Function checkInstance().
*
* Validate given instance.
*
* @param {HookBase} instance
*/
checkInstance( instance ) {
if ( instance.getType() !== this.getType() ) {
throw new Error( `invalid instance, please use: 'elementor-api/modules/hook-base.js'. ` );
}
}
/**
* Function checkId().
*
* Validate if the id is not used before.
*
* @param {string} id
*/
checkId( id ) {
if ( -1 !== this.usedIds.indexOf( id ) ) {
throw Error( `id: '${ id }' is already in use.` );
}
}
/**
* Function shouldRun().
*
* Determine if the event should run.
*
* @param {array} callbacks
*
* @return {boolean}
*
* @throw {Error}
*/
shouldRun( callbacks ) {
return !! callbacks && callbacks.length;
}
/**
* Function register().
*
* Register the callback instance.
*
* @param {string} event
* @param {HookBase} instance
*
* @returns {{}} Created callback
*/
register( event, instance ) {
const command = instance.getCommand(),
id = instance.getId(),
containerType = instance.getContainerType();
this.checkEvent( event );
this.checkInstance( instance );
this.checkId( id );
return this.registerCallback( id, event, command, instance, containerType );
}
/**
* Function registerCallback().
*
* Register callback.
*
* @param {string} id
* @param {string} event
* @param {string} command
* @param {HookBase} instance
* @param {string} containerType
*
* TODO: Consider replace with typedef.
* @returns {{callback: *, id: *, isActive: boolean}}
*/
registerCallback( id, event, command, instance, containerType ) {
if ( ! this.callbacks[ event ][ command ] ) {
this.callbacks[ event ][ command ] = [];
}
// Save used id(s).
this.usedIds.push( id );
if ( ! this.callbacks[ event ][ command ] ) {
this.callbacks[ event ][ command ] = {};
}
// TODO: Create HookCallback class/type.
const callback = {
id,
callback: instance.run.bind( instance ),
isActive: true,
activate: function() {
this.isActive = true;
},
deactivate: function() {
this.isActive = false;
},
};
if ( containerType ) {
if ( ! this.callbacks[ event ][ command ][ containerType ] ) {
this.callbacks[ event ][ command ][ containerType ] = [];
}
this.callbacks[ event ][ command ][ containerType ].push( callback );
} else {
if ( ! this.callbacks[ event ][ command ].all ) {
this.callbacks[ event ][ command ].all = [];
}
this.callbacks[ event ][ command ].all.push( callback );
}
this.callbacksFlatList[ callback.id ] = callback;
return callback;
}
/**
* Function run().
*
* Run the callbacks.
*
* @param {string} event
* @param {string} command
* @param {{}} args
* @param {*} result
*
* @returns {*}
*/
run( event, command, args, result = undefined ) {
const callbacks = this.getCallbacks( event, command, args );
if ( this.shouldRun( callbacks ) ) {
this.current = command;
this.onRun( command, args, event );
return this.runCallbacks( event, command, callbacks, args, result );
}
return false;
}
/**
* Function runCallbacks().
*
* Run's the given callbacks.
*
* @param {string} event
* @param {string} command
* @param {array} callbacks
* @param {{}} args
* @param {[]} result
*/
runCallbacks( event, command, callbacks, args, result ) {
const callbacksResult = [];
for ( const i in callbacks ) {
const callback = callbacks[ i ];
if ( ! callback.isActive ) {
continue;
}
// If not exist, set zero.
if ( undefined === this.depth[ event ][ callback.id ] ) {
this.depth[ event ][ callback.id ] = 0;
}
this.depth[ event ][ callback.id ]++;
// Prevent recursive hooks.
if ( 1 === this.depth[ event ][ callback.id ] ) {
this.onCallback( command, args, event, callback.id );
try {
const callbackResult = this.runCallback( event, callback, args, result );
if ( ! callbackResult ) {
throw Error( `Callback failed, event: '${ event }'` );
}
callbacksResult.push( callbackResult );
} catch ( e ) {
// If its 'Hook-Break' then parent `try {}` will handle it.
if ( e instanceof $e.modules.HookBreak ) {
throw e;
}
elementorCommon.helpers.consoleError( e );
}
}
this.depth[ event ][ callback.id ]--;
}
return callbacksResult;
}
/**
* Function runCallback().
*
* Run's the given callback.
*
* @param {string} event
* @param {{}} callback
* @param {{}} args
* @param {*} result
*
* @returns {*}
*
* @throw {Error}
*/
runCallback( event, callback, args, result ) { // eslint-disable-line no-unused-vars
elementorModules.forceMethodImplementation();
}
/**
* Function onRun().
*
* Called before run a set of callbacks.
*
* @param {string} command
* @param {{}} args
* @param {string} event
*
* @throw {Error}
*/
onRun( command, args, event ) { // eslint-disable-line no-unused-vars
elementorModules.forceMethodImplementation();
}
/**
* Function onCallback().
*
* Called before a single callback.
*
* @param {string} command
* @param {{}} args
* @param {string} event
* @param {string} id
*
* @throw {Error}
*/
onCallback( command, args, event, id ) { // eslint-disable-line no-unused-vars
elementorModules.forceMethodImplementation();
}
}

View File

@@ -0,0 +1,62 @@
import HooksBase from './base.js';
export default class Data extends HooksBase {
constructor( ... args ) {
super( ... args );
this.callbacks.dependency = {};
this.depth.dependency = {};
}
getType() {
return 'data';
}
runCallback( event, callback, args, result ) {
switch ( event ) {
case 'dependency': {
// If callback returns false and its dependency, then 'Hook-Break'.
if ( ! callback.callback( args ) ) {
this.depth[ event ][ callback.id ]--;
// Throw custom break to be catch by the base for 'Safe' exit.
throw new $e.modules.HookBreak;
}
return true;
}
case 'catch':
case 'after': {
/**
* When handling HOOK which is data after (not breakable),
* even the result of the callback is negative, it is required to return positive,
* since result of runCallback determine if the callback succeeded.
*/
return callback.callback( args, result ) || 'after' === event;
}
}
return false;
}
shouldRun( callbacks ) {
return super.shouldRun( callbacks ) && elementor.documents.getCurrent().history.getActive();
}
onRun( command, args, event ) {
if ( ! $e.devTools ) {
return;
}
$e.devTools.log.callbacks().run( this.getType(), command, args, event );
}
onCallback( command, args, event, id ) {
if ( ! $e.devTools ) {
return;
}
$e.devTools.log.callbacks().callback( this.getType(), command, args, event, id );
}
}

View File

@@ -0,0 +1,50 @@
import HooksBase from './base';
export default class Ui extends HooksBase {
constructor( ... args ) {
super( ... args );
this.callbacks.before = {};
this.depth.before = {};
}
getType() {
return 'ui';
}
runCallback( event, callback, args, result ) {
switch ( event ) {
case 'before':
callback.callback( args );
break;
case 'catch':
case 'after':
callback.callback( args, result );
break;
default:
return false;
}
return true;
}
onRun( command, args, event ) {
if ( ! $e.devTools ) {
return;
}
$e.devTools.log.callbacks().run( this.getType(), command, args, event );
}
onCallback( command, args, event, id ) {
if ( ! $e.devTools ) {
return;
}
$e.devTools.log.callbacks().callback( this.getType(), command, args, event, id );
}
}

View File

@@ -0,0 +1,168 @@
import Commands from './commands';
export default class Routes extends Commands {
constructor( ...args ) {
super( ...args );
this.savedStates = {};
this.historyPerComponent = {};
}
refreshContainer( container ) {
const currentRoute = this.getCurrent( container ),
currentArgs = this.getCurrentArgs( container );
this.clearCurrent( container );
this.to( currentRoute, currentArgs );
}
getHistory( namespaceRoot = '' ) {
if ( namespaceRoot ) {
return this.historyPerComponent[ namespaceRoot ] || [];
}
return this.historyPerComponent;
}
clearHistory( namespaceRoot ) {
delete this.historyPerComponent[ namespaceRoot ];
}
clearCurrent( container ) {
const route = this.current[ container ];
if ( ! route ) {
return;
}
delete this.current[ container ];
delete this.currentArgs[ container ];
this.getComponent( route ).onCloseRoute( route );
}
clear() {
Object.keys( this.current ).forEach( ( container ) => this.clearCurrent( container ) );
}
saveState( container ) {
this.savedStates[ container ] = {
route: this.current[ container ],
args: this.currentArgs[ container ],
};
return this;
}
restoreState( container ) {
if ( ! this.savedStates[ container ] ) {
return false;
}
this.to( this.savedStates[ container ].route, this.savedStates[ container ].args );
return true;
}
beforeRun( route, args ) {
if ( ! super.beforeRun( route, args ) ) {
return false;
}
if ( this.is( route, args ) && ! args.refresh ) {
return false;
}
const component = this.getComponent( route ),
container = component.getRootContainer(),
oldRoute = this.current[ container ];
if ( oldRoute ) {
this.getComponent( oldRoute ).onCloseRoute( oldRoute );
}
if ( ! component.isOpen || args.reOpen ) {
component.isOpen = component.open( args );
}
return component.isOpen;
}
to( route, args ) {
this.run( route, args );
const namespaceRoot = this.getComponent( route ).getRootContainer();
if ( ! this.historyPerComponent[ namespaceRoot ] ) {
this.historyPerComponent[ namespaceRoot ] = [];
}
this.historyPerComponent[ namespaceRoot ].push( {
route,
args,
} );
}
back( namespaceRoot ) {
const history = this.getHistory( namespaceRoot );
// Remove current;
history.pop();
const last = history.pop();
if ( ! last ) {
return;
}
this.to( last.route, last.args );
}
// Don't use the event object.
runShortcut( command ) {
this.to( command );
}
// Don't clear current route.
afterRun( route, args ) {
this.getComponent( route ).onRoute( route, args );
}
is( route, args = {} ) {
if ( ! super.is( route ) ) {
return false;
}
const container = this.getComponent( route ).getRootContainer();
return _.isEqual( args, this.currentArgs[ container ] );
}
isPartOf( route ) {
/**
* Check against current command hierarchically.
* For example `is( 'panel' )` will be true for `panel/elements`
* `is( 'panel/editor' )` will be true for `panel/editor/style`
*/
const parts = route.split( '/' ),
container = parts[ 0 ],
toCheck = [],
currentParts = this.current[ container ] ? this.current[ container ].split( '/' ) : [];
let match = false;
currentParts.forEach( ( part ) => {
toCheck.push( part );
if ( toCheck.join( '/' ) === route ) {
match = true;
}
} );
return match;
}
error( message ) {
throw Error( 'Routes: ' + message );
}
}

View File

@@ -0,0 +1,179 @@
import environment from 'elementor-common/utils/environment';
export default class Shortcuts {
constructor( $window ) {
this.specialKeys = {
13: 'enter',
27: 'esc',
38: 'up',
40: 'down',
46: 'del',
191: '?',
};
this.component = '';
this.handlers = {};
this.bindListener( $window );
}
bindListener( $window ) {
$window.on( 'keydown', ( event ) => this.handle( event ) );
}
getAll() {
const shortcuts = {};
jQuery.each( this.handlers, ( key, handler ) => {
jQuery.each( handler, ( index, config ) => {
shortcuts[ config.command ] = key;
} );
} );
return shortcuts;
}
/**
* @param shortcuts
* @param {{callback: (function(): boolean), scopes: [void]}} args
* @param {callback} args.callback Required
* @param {string} args.component Optional
* @param {callback} args.dependency Optional
* @param {array} args.exclude Optional
* @param {bool} args.allowAltKey Optional
*/
register( shortcuts, args ) {
shortcuts.replace( ' ', '' ).split( ',' ).forEach( ( shortcut ) => {
if ( ! this.handlers[ shortcut ] ) {
this.handlers[ shortcut ] = [];
}
this.handlers[ shortcut ].push( args );
} );
}
unregister( shortcuts, args ) {
shortcuts.replace( ' ', '' ).split( ',' ).forEach( ( shortcut ) => {
this.handlers[ shortcut ].forEach( ( index, handler ) => {
if ( args === handler ) {
delete this.handlers[ shortcut ][ index ];
}
} );
} );
}
handle( event ) {
const handlers = this.getHandlersByPriority( event );
if ( ! handlers ) {
return;
}
const filteredHandlers = handlers.filter( ( handler ) => {
if ( handler.exclude && -1 !== handler.exclude.indexOf( 'input' ) ) {
const $target = jQuery( event.target );
if ( $target.is( ':input, .elementor-input' ) || $target.closest( '[contenteditable="true"]' ).length ) {
return false;
}
}
if ( handler.dependency && ! handler.dependency( event ) ) {
return false;
}
// Fix for some keyboard sources that consider alt key as ctrl key
if ( ! handler.allowAltKey && event.altKey ) {
return false;
}
return true;
} );
if ( ! filteredHandlers.length ) {
return;
}
if ( 1 < filteredHandlers.length && elementorCommon.config.isDebug ) {
elementorCommon.helpers.consoleWarn( 'Multiple handlers for shortcut.', filteredHandlers, event );
}
event.preventDefault();
filteredHandlers[ 0 ].callback( event );
}
isControlEvent( event ) {
return event[ environment.mac ? 'metaKey' : 'ctrlKey' ];
}
getEventShortcut( event ) {
const shortcut = [];
if ( event.altKey ) {
shortcut.push( 'alt' );
}
if ( this.isControlEvent( event ) ) {
shortcut.push( 'ctrl' );
}
if ( event.shiftKey ) {
shortcut.push( 'shift' );
}
if ( this.specialKeys[ event.which ] ) {
shortcut.push( this.specialKeys[ event.which ] );
} else {
shortcut.push( String.fromCharCode( event.which ).toLowerCase() );
}
return shortcut.join( '+' );
}
isActiveScope( scopes ) {
const activeComponents = Object.keys( $e.components.activeComponents ),
activeComponent = activeComponents[ activeComponents.length - 1 ],
component = $e.components.get( activeComponent );
if ( ! component ) {
return false;
}
const namespace = component.getNamespace(),
namespaceRoot = component.getRootContainer();
const filteredByNamespace = scopes.some( ( scope ) => namespace === scope );
if ( filteredByNamespace ) {
return true;
}
// Else filter by namespaceRoot.
return scopes.some( ( scope ) => namespaceRoot === scope );
}
getHandlersByPriority( event ) {
const handlers = this.handlers[ this.getEventShortcut( event ) ];
if ( ! handlers ) {
return false;
}
// TODO: Prioritize current scope before roo scope.
const inCurrentScope = handlers.filter( ( handler ) => {
return handler.scopes && this.isActiveScope( handler.scopes );
} );
if ( inCurrentScope.length ) {
return inCurrentScope;
}
const noScope = handlers.filter( ( handler ) => {
return ! handler.scopes;
} );
if ( noScope.length ) {
return noScope;
}
}
}

View File

@@ -0,0 +1,107 @@
/**
* @typedef HashCommand
* @property {string} method,
* @property {string} command
*/
export default class HashCommands {
/**
* Cannot be static since it uses callback(s) that are available only after '$e' is initialized.
*/
dispatchersList = {
'e:run': {
runner: $e.run,
isSafe: ( command ) => $e.commands.getCommandClass( command )?.getInfo().isSafe,
},
'e:route': {
runner: $e.route,
isSafe: () => true,
},
};
/**
* List of current loaded hash commands.
*
* @type {Array.<HashCommand>}
*/
commands = [];
constructor() {
this.commands = this.get();
}
/**
* Function get().
*
* Get API requests that comes from hash ( eg #e:run ).
*
* @param {string} hash
*
* @returns {Array.<HashCommand>}
*/
get( hash = location.hash ) {
const result = [];
if ( hash ) {
// Remove first '#' and split each '&'.
const hashList = hash.substr( 1 ).split( '&' );
hashList.forEach( ( hashItem ) => {
const hashParts = hashItem.split( ':' );
if ( 3 !== hashParts.length ) {
return;
}
const method = hashParts[ 0 ] + ':' + hashParts[ 1 ],
dispatcher = this.dispatchersList[ method ];
if ( dispatcher ) {
const command = hashParts[ 2 ];
result.push( {
method,
command,
} );
}
} );
}
return result;
}
/**
* Function run().
*
* Run API requests that comes from hash ( eg #e:run ).
*
* @param {Array.<HashCommand>} [commands=this.commands]
*/
run( commands = this.commands ) {
commands.forEach( ( hashCommand ) => {
const dispatcher = this.dispatchersList[ hashCommand.method ];
if ( ! dispatcher ) {
throw Error( `No dispatcher found for the command: \`${ hashCommand.command }\`.` );
}
if ( ! dispatcher.isSafe( hashCommand.command ) ) {
throw Error( `Attempting to run unsafe or non exist command: \`${ hashCommand.command }\`.` );
}
dispatcher.runner( hashCommand.command );
} );
}
/**
* Function runOnce().
*
* Do same as `run` but clear `this.commands` before leaving.
*/
runOnce() {
this.run( this.commands );
this.commands = [];
}
}

View File

@@ -0,0 +1,104 @@
/* Alphabetical order */
import BackwardsCompatibility from './core/backwards-compatibility';
import CommandBase from './modules/command-base';
import CommandInternalBase from './modules/command-internal-base';
import CommandData from './modules/command-data';
import Commands from './core/commands';
import CommandsInternal from './core/commands-internal';
import ComponentBase from './modules/component-base';
import ComponentModalBase from './modules/component-modal-base';
import Components from './core/components';
import Data from './core/data.js';
import HashCommands from './extras/hash-commands';
import HookBreak from './modules/hook-break';
import Hooks from './core/hooks';
import Routes from './core/routes';
import Shortcuts from './core/shortcuts';
import * as hookData from './modules/hooks/data/';
import * as hookUI from './modules/hooks/ui';
export default class API {
/**
* Function constructor().
*
* Create's 'elementor' api.
*/
constructor() {
window.$e = this;
this.components = new Components();
this.commands = new Commands();
this.commandsInternal = new CommandsInternal();
this.hooks = new Hooks();
this.routes = new Routes();
this.shortcuts = new Shortcuts( jQuery( window ) );
this.data = new Data();
this.modules = {
CommandBase,
CommandInternalBase,
CommandData,
ComponentBase,
ComponentModalBase,
HookBreak,
hookData,
hookUI,
};
this.extras = {
hashCommands: new HashCommands(),
};
// Backwards compatibility should be last, in order to handle others.
this.bc = new BackwardsCompatibility();
}
/**
* Function run().
*
* Alias of `$e.commands.run()`.
*
* @param {string} command
* @param [args={}]
*
* @returns {*}
*/
run( command, args = {} ) {
return $e.commands.run( command, args );
}
/**
* Function internal().
*
* Alias of `$e.commandsInternal.run()`.
*
* @param {string} command
* @param [args={}]
*
* @returns {boolean}
*/
internal( command, args = {} ) {
return $e.commandsInternal.run( command, args );
}
/**
* Function route().
*
* Alias of `$e.routes.to()`.
*
* @param {string} route
* @param [args={}]
*/
route( route, args = {} ) {
return $e.routes.to( route, args );
}
// TODO: shortcut();
}

View File

@@ -0,0 +1,270 @@
import ArgsObject from 'elementor-assets-js/modules/imports/args-object';
export default class CommandBase extends ArgsObject {
static getInstanceType() {
return 'CommandBase';
}
/**
* Get info of command.
*
* Use to provide 'extra' information about the command.
*
* @returns {Object}
*/
static getInfo() {
return {};
}
/**
* Current component.
*
* @type {Component}
*/
component;
/**
* Function constructor().
*
* Create Commands Base.
*
* @param [args={}]
* @param [commandsAPI={}]
*/
constructor( args, commandsAPI = $e.commands ) {
super( args );
// Acknowledge self about which command it run.
this.currentCommand = commandsAPI.getCurrentLast();
// Assign instance of current component.
this.component = commandsAPI.getComponent( this.currentCommand );
// Who ever need do something before without `super` the constructor can use `initialize` method.
this.initialize( args );
// Refresh args, maybe the changed via `initialize`.
args = this.args;
// Validate args before run.
this.validateArgs( args );
}
/**
* Function requireContainer().
*
* Validate `arg.container` & `arg.containers`.
*
* @param {{}} args
*
* @throws {Error}
*/
requireContainer( args = this.args ) {
if ( ! args.container && ! args.containers ) {
throw Error( 'container or containers are required.' );
}
if ( args.container && args.containers ) {
throw Error( 'container and containers cannot go together please select one of them.' );
}
const containers = args.containers || [ args.container ];
containers.forEach( ( container ) => {
this.requireArgumentInstance( 'container', elementorModules.editor.Container, { container } );
} );
}
/**
* Function initialize().
*
* Initialize command, called after construction.
*
* @param [args={}]
*/
initialize( args = {} ) {} // eslint-disable-line no-unused-vars
/**
* Function validateArgs().
*
* Validate command arguments.
*
* @param [args={}]
*/
validateArgs( args = {} ) {} // eslint-disable-line no-unused-vars
/**
* Function isDataChanged().
*
* Whether the editor needs to set change flag on/off.
*
* @returns {boolean}
*/
isDataChanged() {
return false;
}
/**
* Function apply().
*
* Do the actual command.
*
* @param [args={}]
*
* @returns {*}
*/
apply( args = {} ) { // eslint-disable-line no-unused-vars
elementorModules.ForceMethodImplementation();
}
/**
* Function run().
*
* Run command with history & hooks.
*
* @returns {*}
*/
run() {
let result;
// For UI Hooks.
this.onBeforeRun( this.args );
try {
// For Data hooks.
this.onBeforeApply( this.args );
result = this.apply( this.args );
} catch ( e ) {
this.onCatchApply( e );
// Catch 'Hook-Break' that comes from hooks base.
if ( e instanceof $e.modules.HookBreak ) {
// Bypass.
return false;
}
}
return this.runAfter( result );
}
runAfter( result ) {
const onAfter = ( _result ) => {
// Run Data hooks.
this.onAfterApply( this.args, _result );
// TODO: Create Command-Base for Command-Document and apply it on after.
if ( this.isDataChanged() ) {
$e.internal( 'document/save/set-is-modified', { status: true } );
}
// For UI hooks.
this.onAfterRun( this.args, _result );
},
asyncOnAfter = async ( _result ) => {
// Run Data hooks.
const results = this.onAfterApply( this.args, _result ),
promises = Array.isArray( results ) ? results.flat().filter( ( filtered ) => filtered instanceof Promise ) : [];
if ( promises.length ) {
// Wait for hooks before return the value.
await Promise.all( promises );
}
if ( this.isDataChanged() ) {
// TODO: Create Command-Base for Command-Document and apply it on after.
$e.internal( 'document/save/set-is-modified', { status: true } );
}
// For UI hooks.
this.onAfterRun( this.args, _result );
};
// TODO: Temp code determine if it's a jQuery deferred object.
if ( result && 'object' === typeof result && result.promise && result.then && result.fail ) {
result.fail( this.onCatchApply.bind( this ) );
result.done( onAfter );
} else if ( result instanceof Promise ) {
// Override initial result ( promise ) to await onAfter promises, first!.
return ( async () => {
await result.catch( this.onCatchApply.bind( this ) );
await result.then( ( _result ) => asyncOnAfter( _result ) );
return result;
} )();
} else {
onAfter( result );
}
return result;
}
/**
* Run all the catch hooks.
*
* @param {Error} e
*/
runCatchHooks( e ) {
$e.hooks.runDataCatch( this.currentCommand, this.args, e );
$e.hooks.runUICatch( this.currentCommand, this.args, e );
}
/**
* Function onBeforeRun.
*
* Called before run().
*
* @param [args={}]
*/
onBeforeRun( args = {} ) {
$e.hooks.runUIBefore( this.currentCommand, args );
}
/**
* Function onAfterRun.
*
* Called after run().
*
* @param [args={}]
* @param [result={*}]
*/
onAfterRun( args = {}, result ) {
$e.hooks.runUIAfter( this.currentCommand, args, result );
}
/**
* Function onBeforeApply.
*
* Called before apply().
*
* @param [args={}]
*/
onBeforeApply( args = {} ) {
$e.hooks.runDataDependency( this.currentCommand, args );
}
/**
* Function onAfterApply.
*
* Called after apply().
*
* @param [args={}]
* @param [result={*}]
*/
onAfterApply( args = {}, result ) {
return $e.hooks.runDataAfter( this.currentCommand, args, result );
}
/**
* Function onCatchApply.
*
* Called after apply() failed.
*
* @param {Error} e
*/
onCatchApply( e ) {
this.runCatchHooks( e );
elementorCommon.helpers.consoleError( e );
}
}

View File

@@ -0,0 +1,226 @@
import CommandBase from './command-base';
import * as errors from './errors';
export default class CommandData extends CommandBase {
/**
* Data returned from remote.
*
* @type {*}
*/
data;
/**
* Fetch type.
*
* @type {DataTypes}
*/
type;
static getInstanceType() {
return 'CommandData';
}
constructor( args, commandsAPI = $e.data ) {
super( args, commandsAPI );
if ( this.args.options?.type ) {
this.type = this.args.options.type;
}
}
/**
* Function getEndpointFormat().
*
* @returns {(null|string)}
*/
static getEndpointFormat() {
return null;
}
/**
* @param {DataTypes} type
*
* @returns {boolean|{before: (function(*=): {}), after: (function({}, *=): {})}}
*/
getApplyMethods( type = this.type ) {
let before, after;
switch ( type ) {
case 'create':
before = this.applyBeforeCreate;
after = this.applyAfterCreate;
break;
case 'delete':
before = this.applyBeforeDelete;
after = this.applyAfterDelete;
break;
case 'get':
before = this.applyBeforeGet;
after = this.applyAfterGet;
break;
case 'update':
before = this.applyBeforeUpdate;
after = this.applyAfterUpdate;
break;
case 'options':
before = this.applyBeforeOptions;
after = this.applyAfterOptions;
break;
default:
return false;
}
return { before, after };
}
/**
* Function getRequestData().
*
* @returns {RequestData}
*/
getRequestData() {
return {
type: this.type,
args: this.args,
timestamp: new Date().getTime(),
component: this.component,
command: this.currentCommand,
endpoint: $e.data.commandToEndpoint( this.currentCommand, elementorCommon.helpers.cloneObject( this.args ), this.constructor.getEndpointFormat() ),
};
}
apply() {
const applyMethods = this.getApplyMethods();
// Run 'before' method.
this.args = applyMethods.before( this.args );
const requestData = this.getRequestData();
return $e.data.fetch( requestData ).then( ( data ) => {
this.data = data;
// Run 'after' method.
this.data = applyMethods.after( data, this.args );
this.data = { data: this.data };
// Append requestData.
this.data = Object.assign( { __requestData__: requestData }, this.data );
return this.data;
} );
}
/**
* @param [args={}]
* @returns {{}} filtered args
*/
applyBeforeCreate( args = {} ) {
return args;
}
/**
* @param {{}} data
* @param [args={}]
* @returns {{}} filtered result
*/
applyAfterCreate( data, args = {} ) {// eslint-disable-line no-unused-vars
return data;
}
/**
* @param [args={}]
* @returns {{}} filtered args
*/
applyBeforeDelete( args = {} ) {
return args;
}
/**
* @param {{}} data
* @param [args={}]
* @returns {{}} filtered result
*/
applyAfterDelete( data, args = {} ) {// eslint-disable-line no-unused-vars
return data;
}
/**
* @param [args={}]
* @returns {{}} filtered args
*/
applyBeforeGet( args = {} ) {
return args;
}
/**
* @param {{}} data
* @param [args={}]
* @returns {{}} filtered result
*/
applyAfterGet( data, args = {} ) {// eslint-disable-line no-unused-vars
return data;
}
/**
* @param [args={}]
* @returns {{}} filtered args
*/
applyBeforeUpdate( args = {} ) {
return args;
}
/**
* @param {{}} data
* @param [args={}]
* @returns {{}} filtered result
*/
applyAfterUpdate( data, args = {} ) {// eslint-disable-line no-unused-vars
return data;
}
/**
* @param [args={}]
* @returns {{}} filtered args
*/
applyBeforeOptions( args = {} ) {
return args;
}
/**
* @param {{}} data
* @param [args={}]
* @returns {{}} filtered result
*/
applyAfterOptions( data, args = {} ) {// eslint-disable-line no-unused-vars
return data;
}
/**
* Called after apply() failed.
*
* @param e
*/
onCatchApply( e ) {
// TODO: If the errors that returns from the server is consistent remove the '?' from 'e'
const status = e?.data?.status || 0;
let dataError = Object.values( errors )
.find( ( error ) => error.getStatus() === status );
if ( ! dataError ) {
dataError = errors.DefaultError;
}
e = dataError.create( e.message, e.code, e.data || [] );
this.runCatchHooks( e );
e.notify();
}
}

View File

@@ -0,0 +1,11 @@
import CommandBase from './command-base';
export default class CommandInternalBase extends CommandBase {
static getInstanceType() {
return 'CommandInternalBase';
}
constructor( args, commandsAPI = $e.commandsInternal ) {
super( args, commandsAPI );
}
}

View File

@@ -0,0 +1,9 @@
import CommandBase from 'elementor-api/modules/command-base';
export class Close extends CommandBase {
apply() {
this.component.close();
}
}
export default Close;

View File

@@ -0,0 +1,3 @@
export { Close } from './close';
export { Open } from './open';
export { Toggle } from './toggle';

View File

@@ -0,0 +1,9 @@
import CommandBase from 'elementor-api/modules/command-base';
export class Open extends CommandBase {
apply() {
$e.route( this.component.getNamespace() );
}
}
export default Open;

View File

@@ -0,0 +1,13 @@
import CommandBase from 'elementor-api/modules/command-base';
export class Toggle extends CommandBase {
apply() {
if ( this.component.isOpen ) {
this.component.close();
} else {
$e.route( this.component.getNamespace() );
}
}
}
export default Toggle;

View File

@@ -0,0 +1,302 @@
export default class ComponentBase extends elementorModules.Module {
__construct( args = {} ) {
if ( args.manager ) {
this.manager = args.manager;
}
this.commands = this.defaultCommands();
this.commandsInternal = this.defaultCommandsInternal();
this.hooks = this.defaultHooks();
this.routes = this.defaultRoutes();
this.tabs = this.defaultTabs();
this.shortcuts = this.defaultShortcuts();
this.utils = this.defaultUtils();
this.data = this.defaultData();
this.defaultRoute = '';
this.currentTab = '';
}
registerAPI() {
Object.entries( this.getTabs() ).forEach( ( tab ) => this.registerTabRoute( tab[ 0 ] ) );
Object.entries( this.getRoutes() ).forEach( ( [ route, callback ] ) => this.registerRoute( route, callback ) );
Object.entries( this.getCommands() ).forEach( ( [ command, callback ] ) => this.registerCommand( command, callback ) );
Object.entries( this.getCommandsInternal() ).forEach( ( [ command, callback ] ) => this.registerCommandInternal( command, callback ) );
Object.values( this.getHooks() ).forEach( ( instance ) => this.registerHook( instance ) );
Object.entries( this.getData() ).forEach( ( [ command, callback ] ) => this.registerData( command, callback ) );
}
/**
* @returns {string}
*/
getNamespace() {
elementorModules.ForceMethodImplementation();
}
getRootContainer() {
const parts = this.getNamespace().split( '/' );
return parts[ 0 ];
}
defaultTabs() {
return {};
}
defaultRoutes() {
return {};
}
defaultCommands() {
return {};
}
defaultCommandsInternal() {
return {};
}
defaultHooks() {
return {};
}
defaultShortcuts() {
return {};
}
defaultUtils() {
return {};
}
defaultData() {
return {};
}
getCommands() {
return this.commands;
}
getCommandsInternal() {
return this.commandsInternal;
}
getHooks() {
return this.hooks;
}
getRoutes() {
return this.routes;
}
getTabs() {
return this.tabs;
}
getShortcuts() {
return this.shortcuts;
}
getData() {
return this.data;
}
registerCommand( command, callback ) {
$e.commands.register( this, command, callback );
}
/**
* @param {HookBase} instance
*/
registerHook( instance ) {
return instance.register();
}
registerCommandInternal( command, callback ) {
$e.commandsInternal.register( this, command, callback );
}
registerRoute( route, callback ) {
$e.routes.register( this, route, callback );
}
registerData( command, callback ) {
$e.data.register( this, command, callback );
}
unregisterRoute( route ) {
$e.routes.unregister( this, route );
}
registerTabRoute( tab ) {
this.registerRoute( tab, ( args ) => this.activateTab( tab, args ) );
}
dependency() {
return true;
}
open() {
return true;
}
close() {
if ( ! this.isOpen ) {
return false;
}
this.isOpen = false;
this.inactivate();
$e.routes.clearCurrent( this.getNamespace() );
$e.routes.clearHistory( this.getRootContainer() );
return true;
}
activate() {
$e.components.activate( this.getNamespace() );
}
inactivate() {
$e.components.inactivate( this.getNamespace() );
}
isActive() {
return $e.components.isActive( this.getNamespace() );
}
onRoute( route ) {
this.toggleRouteClass( route, true );
this.toggleHistoryClass();
this.activate();
this.trigger( 'route/open', route );
}
onCloseRoute( route ) {
this.toggleRouteClass( route, false );
this.inactivate();
this.trigger( 'route/close', route );
}
setDefaultRoute( route ) {
this.defaultRoute = this.getNamespace() + '/' + route;
}
getDefaultRoute() {
return this.defaultRoute;
}
removeTab( tab ) {
delete this.tabs[ tab ];
this.unregisterRoute( tab );
}
hasTab( tab ) {
return ! ! this.tabs[ tab ];
}
addTab( tab, args, position ) {
this.tabs[ tab ] = args;
// It can be 0.
if ( 'undefined' !== typeof position ) {
const newTabs = {};
const ids = Object.keys( this.tabs );
// Remove new tab
ids.pop();
// Add it to position.
ids.splice( position, 0, tab );
ids.forEach( ( id ) => {
newTabs[ id ] = this.tabs[ id ];
} );
this.tabs = newTabs;
}
this.registerTabRoute( tab );
}
getTabsWrapperSelector() {
return '';
}
getTabRoute( tab ) {
return this.getNamespace() + '/' + tab;
}
renderTab( tab ) {} // eslint-disable-line
activateTab( tab, args ) {
this.currentTab = tab;
this.renderTab( tab, args );
jQuery( this.getTabsWrapperSelector() + ' .elementor-component-tab' )
.off( 'click' )
.on( 'click', ( event ) => {
$e.route( this.getTabRoute( event.currentTarget.dataset.tab ), args );
} )
.removeClass( 'elementor-active' )
.filter( '[data-tab="' + tab + '"]' )
.addClass( 'elementor-active' );
}
getActiveTabConfig() {
return this.tabs[ this.currentTab ] || {};
}
getBodyClass( route ) {
return 'e-route-' + route.replace( /\//g, '-' );
}
/**
* If command includes uppercase character convert it to lowercase and add `-`.
* e.g: `CopyAll` is converted to `copy-all`.
*/
normalizeCommandName( commandName ) {
return commandName.replace( /[A-Z]/g, ( match, offset ) => ( offset > 0 ? '-' : '' ) + match.toLowerCase() );
}
importCommands( commandsFromImport ) {
const commands = {};
// Convert `Commands` to `ComponentBase` workable format.
Object.entries( commandsFromImport ).forEach( ( [ className, Class ] ) => {
const command = this.normalizeCommandName( className );
commands[ command ] = ( args ) => ( new Class( args ) ).run();
// TODO: Temporary code, remove after merge with 'require-commands-base' branch.
// should not return callback, but Class or Instance without run ( gain performance ).
$e.commands.classes[ this.getNamespace() + '/' + command ] = Class;
} );
return commands;
}
importHooks( hooksFromImport ) {
const hooks = {};
for ( const key in hooksFromImport ) {
const hook = new hooksFromImport[ key ];
hooks[ hook.getId() ] = hook;
}
return hooks;
}
toggleRouteClass( route, state ) {
elementorCommon.elements.$body.toggleClass( this.getBodyClass( route ), state );
}
toggleHistoryClass() {
elementorCommon.elements.$body.toggleClass( 'e-routes-has-history', !! $e.routes.getHistory( this.getRootContainer() ).length );
}
}

View File

@@ -0,0 +1,50 @@
import ComponentBase from './component-base';
import * as commands from './commands/';
export default class ComponentModalBase extends ComponentBase {
registerAPI() {
super.registerAPI();
$e.shortcuts.register( 'esc', {
scopes: [ this.getNamespace() ],
callback: () => this.close(),
} );
}
defaultCommands() {
return this.importCommands( commands );
}
defaultRoutes() {
return {
'': () => { /* Nothing to do, it's already rendered. */ },
};
}
open() {
if ( ! this.layout ) {
const layout = this.getModalLayout();
this.layout = new layout( { component: this } );
this.layout.getModal().on( 'hide', () => this.close() );
}
this.layout.showModal();
return true;
}
close() {
if ( ! super.close() ) {
return false;
}
this.layout.getModal().hide();
return true;
}
getModalLayout() {
elementorModules.ForceMethodImplementation();
}
}

View File

@@ -0,0 +1,55 @@
export default class BaseError extends Error {
/**
* The server error code.
*
* @type {string}
*/
code = '';
/**
* Additional data about the current error.
*
* @type {*[]}
*/
data = [];
/**
* Static helper function to create the error.
*
* @param message
* @param code
* @param data
* @returns {BaseError}
*/
static create( message, code = '', data = [] ) {
return new this( message, code, data );
}
/**
* Returns the status code of the error.
*/
static getStatus() {
elementorModules.ForceMethodImplementation();
}
/**
* Error constructor.
*
* @param code
* @param message
* @param data
*/
constructor( message = '', code = '', data = [] ) {
super( message );
this.code = code;
this.data = data;
}
/**
* Notify a message when the error occurs.
*/
notify() {
elementorCommon.helpers.consoleError( { message: this.message, ...this } );
}
}

View File

@@ -0,0 +1,9 @@
import BaseError from './base-error';
export class DefaultError extends BaseError {
static getStatus() {
return 0;
}
}
export default DefaultError;

View File

@@ -0,0 +1,2 @@
export { NotFoundError } from './not-found-error';
export { DefaultError } from './default-error';

View File

@@ -0,0 +1,13 @@
import BaseError from './base-error';
export class NotFoundError extends BaseError {
static getStatus() {
return 404;
}
notify() {
elementorCommon.helpers.consoleWarn( this.message );
}
}
export default NotFoundError;

View File

@@ -0,0 +1,161 @@
export default class HookBase {
/**
* Callback type, eg ( hook, event ).
*
* @type {string}
*/
type;
/**
* Full command address, that will hook the callback.
*
* @type (string)
*/
command;
/**
* Unique id of the callback.
*
* @type {string}
*/
id;
/**
* Function constructor().
*
* Create callback base.
*/
constructor() {
this.initialize();
this.type = this.getType();
this.command = this.getCommand();
this.id = this.getId();
}
/**
* Function initialize().
*
* Called after creation of the base, used for initialize extras.
* Without expending constructor.
*/
initialize() {}
/**
* Function register().
*
* Used to register the callback.
*
* @throws {Error}
*/
register() {
elementorModules.ForceMethodImplementation();
}
/**
* Function getType().
*
* Get type eg: ( hook, event, etc ... ).
*
* @returns {string}
*
* @throws {Error}
*/
getType() {
elementorModules.ForceMethodImplementation();
}
/**
* Function getCommand().
*
* Returns the full command path for callback binding.
*
* Supports array of strings ( commands ).
*
* @returns {string}
*
* @throws {Error}
*/
getCommand() {
elementorModules.ForceMethodImplementation();
}
/**
* Function getId().
*
* Returns command id for the hook (should be unique).
*
* @returns {string}
*
* @throws {Error}
*/
getId() {
elementorModules.ForceMethodImplementation();
}
/**
* Function getContainerType().
*
* Bind eContainer type to callback.
*
* Used to gain performance.
*
* @return {string} type
*/
getContainerType() {}
/**
* Function getConditions().
*
* Condition for running the callback, if true, call to apply().
*
* @param [args={}]
* @param [result=*]
*
* @returns {boolean}
*/
getConditions( args = {}, result ) { // eslint-disable-line no-unused-vars
return true;
}
/**
* Function apply().
*
* Apply the callback, ( The actual affect of the callback ).
*
* @param [args={}]
*
* @returns {*}
*/
apply( args ) { // eslint-disable-line no-unused-vars
elementorModules.ForceMethodImplementation();
}
/**
* Function run().
*
* Run the callback.
*
* @param {*} args
*
* @returns {*}
*/
run( ... args ) {
const { options = {} } = args[ 0 ];
// Disable callback if requested by args.options.
if ( options.callbacks && false === options.callbacks[ this.id ] ) {
return true;
}
if ( this.getConditions( ... args ) ) {
if ( $e.devTools ) {
$e.devTools.log.callbacks().active( this.type, this.command, this.id );
}
return this.apply( ... args );
}
return true;
}
}

View File

@@ -0,0 +1,5 @@
export default class HookBreak extends Error {
constructor() {
super( 'HookBreak' );
}
}

View File

@@ -0,0 +1,9 @@
import Base from './base';
export class After extends Base {
register() {
$e.hooks.registerDataAfter( this );
}
}
export default After;

View File

@@ -0,0 +1,9 @@
import HookBase from 'elementor-api/modules/hook-base';
export class Base extends HookBase {
getType() {
return 'data';
}
}
export default Base;

View File

@@ -0,0 +1,9 @@
import Base from './base';
export class Catch extends Base {
register() {
$e.hooks.registerDataCatch( this );
}
}
export default Catch;

View File

@@ -0,0 +1,9 @@
import Base from './base';
export class Dependency extends Base {
register() {
$e.hooks.registerDataDependency( this );
}
}
export default Dependency;

View File

@@ -0,0 +1,6 @@
// Alphabetical order.
export { After } from './after';
export { Base } from './base';
export { Catch } from './catch';
export { Dependency } from './dependency';

View File

@@ -0,0 +1,9 @@
import Base from './base';
export class After extends Base {
register() {
$e.hooks.registerUIAfter( this );
}
}
export default After;

View File

@@ -0,0 +1,9 @@
import HookBase from 'elementor-api/modules/hook-base';
export class Base extends HookBase {
getType() {
return 'ui';
}
}
export default Base;

View File

@@ -0,0 +1,9 @@
import Base from './base';
export class Before extends Base {
register() {
$e.hooks.registerUIBefore( this );
}
}
export default Before;

View File

@@ -0,0 +1,9 @@
import Base from './base';
export class Catch extends Base {
register() {
$e.hooks.registerUICatch( this );
}
}
export default Catch;

View File

@@ -0,0 +1,6 @@
// Alphabetical order.
export { After } from './after';
export { Base } from './base';
export { Before } from './before';
export { Catch } from './catch';

View File

@@ -0,0 +1,114 @@
import Helpers from './utils/helpers';
import Storage from './utils/storage';
import Debug from './utils/debug';
import Ajax from 'elementor-common-modules/ajax/assets/js/ajax';
import Finder from 'elementor-common-modules/finder/assets/js/finder';
import Connect from 'elementor-common-modules/connect/assets/js/connect';
import API from './api/';
class ElementorCommonApp extends elementorModules.ViewModule {
setMarionetteTemplateCompiler() {
Marionette.TemplateCache.prototype.compileTemplate = ( rawTemplate, options ) => {
options = {
evaluate: /<#([\s\S]+?)#>/g,
interpolate: /{{{([\s\S]+?)}}}/g,
escape: /{{([^}]+?)}}(?!})/g,
};
return _.template( rawTemplate, options );
};
}
getDefaultElements() {
return {
$window: jQuery( window ),
$document: jQuery( document ),
$body: jQuery( document.body ),
};
}
initComponents() {
this.debug = new Debug();
this.helpers = new Helpers();
this.storage = new Storage();
this.dialogsManager = new DialogsManager.Instance();
this.api = new API();
this.initModules();
}
initModules() {
const { activeModules } = this.config;
const modules = {
ajax: Ajax,
finder: Finder,
connect: Connect,
};
activeModules.forEach( ( name ) => {
if ( modules[ name ] ) {
this[ name ] = new modules[ name ]( this.config[ name ] );
}
} );
}
compileArrayTemplateArgs( template, templateArgs ) {
return template.replace( /%(?:(\d+)\$)?s/g, ( match, number ) => {
if ( ! number ) {
number = 1;
}
number--;
return undefined !== templateArgs[ number ] ? templateArgs[ number ] : match;
} );
}
compileObjectTemplateArgs( template, templateArgs ) {
return template.replace( /{{(?:([ \w]+))}}/g, ( match, name ) => {
return templateArgs[ name ] ? templateArgs[ name ] : match;
} );
}
compileTemplate( template, templateArgs ) {
return jQuery.isPlainObject( templateArgs ) ? this.compileObjectTemplateArgs( template, templateArgs ) : this.compileArrayTemplateArgs( template, templateArgs );
}
translate( stringKey, context, templateArgs, i18nStack ) {
if ( context ) {
i18nStack = this.config[ context ].i18n;
}
if ( ! i18nStack ) {
i18nStack = this.config.i18n;
}
let string = i18nStack[ stringKey ];
if ( undefined === string ) {
string = stringKey;
}
if ( templateArgs ) {
string = this.compileTemplate( string, templateArgs );
}
return string;
}
onInit() {
super.onInit();
this.config = elementorCommonConfig;
this.setMarionetteTemplateCompiler();
}
}
window.elementorCommon = new ElementorCommonApp();
elementorCommon.initComponents();

View File

@@ -0,0 +1,40 @@
import elementorModules from 'elementor-assets-js/modules/modules';
import ModalLayout from './views/modal/layout';
import ComponentBase from './api/modules/component-base';
import ComponentModalBase from 'elementor-api/modules/component-modal-base';
import HookBreak from './api/modules/hook-break';
elementorModules.common = {
get Component() {
// `elementorCommon` isn't available during it self initialize.
setTimeout( () => {
elementorCommon.helpers.softDeprecated( 'elementorModules.common.Component', '2.9.0',
'$e.modules.ComponentBase' );
}, 2000 );
return ComponentBase;
},
get ComponentModal() {
// `elementorCommon` isn't available during it self initialize.
setTimeout( () => {
elementorCommon.helpers.softDeprecated( 'elementorModules.common.ComponentModal', '2.9.0',
'$e.modules.ComponentModalBase' );
}, 2000 );
return ComponentModalBase;
},
get HookBreak() {
// `elementorCommon` isn't available during it self initialize.
setTimeout( () => {
elementorCommon.helpers.softDeprecated( 'elementorModules.common.HookBreak', '2.9.0',
'$e.modules.HookBreak' );
}, 2000 );
return HookBreak;
},
views: {
modal: {
Layout: ModalLayout,
},
},
};

View File

@@ -0,0 +1,135 @@
// Moved from assets/dev/js/editor/utils
var Debug = function() {
var self = this,
errorStack = [],
settings = {},
elements = {};
var initSettings = function() {
settings = {
debounceDelay: 500,
urlsToWatch: [
'elementor/assets',
],
};
};
var initElements = function() {
elements.$window = jQuery( window );
};
var onError = function( event ) {
const error = event.originalEvent?.error;
if ( ! error ) {
return;
}
var isInWatchList = false,
urlsToWatch = settings.urlsToWatch;
jQuery.each( urlsToWatch, function() {
if ( -1 !== error.stack.indexOf( this ) ) {
isInWatchList = true;
return false;
}
} );
if ( ! isInWatchList ) {
return;
}
self.addError( {
type: error.name,
message: error.message,
url: originalEvent.filename,
line: originalEvent.lineno,
column: originalEvent.colno,
} );
};
var bindEvents = function() {
elements.$window.on( 'error', onError );
};
var init = function() {
initSettings();
initElements();
bindEvents();
self.sendErrors = _.debounce( self.sendErrors, settings.debounceDelay );
};
this.addURLToWatch = function( url ) {
settings.urlsToWatch.push( url );
};
this.addCustomError = function( error, category, tag ) {
var errorInfo = {
type: error.name,
message: error.message,
url: error.fileName || error.sourceURL,
line: error.lineNumber || error.line,
column: error.columnNumber || error.column,
customFields: {
category: category || 'general',
tag: tag,
},
};
if ( ! errorInfo.url ) {
var stackInfo = error.stack.match( /\n {4}at (.*?(?=:(\d+):(\d+)))/ );
if ( stackInfo ) {
errorInfo.url = stackInfo[ 1 ];
errorInfo.line = stackInfo[ 2 ];
errorInfo.column = stackInfo[ 3 ];
}
}
this.addError( errorInfo );
};
this.addError = function( errorParams ) {
var defaultParams = {
type: 'Error',
timestamp: Math.floor( new Date().getTime() / 1000 ),
message: null,
url: null,
line: null,
column: null,
customFields: {},
};
errorStack.push( jQuery.extend( true, defaultParams, errorParams ) );
self.sendErrors();
};
this.sendErrors = function() {
// Avoid recursions on errors in ajax
elements.$window.off( 'error', onError );
jQuery.ajax( {
url: elementorCommon.config.ajax.url,
method: 'POST',
data: {
action: 'elementor_js_log',
_nonce: elementorCommon.ajax.getSettings( 'nonce' ),
data: errorStack,
},
success: function() {
errorStack = [];
// Restore error handler
elements.$window.on( 'error', onError );
},
} );
};
init();
};
module.exports = Debug;

View File

@@ -0,0 +1,49 @@
const matchUserAgent = ( UserAgentStr ) => {
return userAgent.indexOf( UserAgentStr ) >= 0;
},
userAgent = navigator.userAgent,
// Solution influenced by https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
// Opera 8.0+
isOpera = ( !! window.opr && !! opr.addons ) || !! window.opera || matchUserAgent( ' OPR/' ),
// Firefox 1.0+
isFirefox = matchUserAgent( 'Firefox' ),
// Safari 3.0+ "[object HTMLElementConstructor]"
isSafari = /^((?!chrome|android).)*safari/i.test( userAgent ) || /constructor/i.test( window.HTMLElement ) ||
( ( p ) => {
return '[object SafariRemoteNotification]' === p.toString();
} )( ! window.safari || ( typeof safari !== 'undefined' && safari.pushNotification ) ),
// Internet Explorer 6-11
isIE = /Trident|MSIE/.test( userAgent ) && ( /*@cc_on!@*/false || !! document.documentMode ),
// Edge 20+
isEdge = ( ! isIE && !! window.StyleMedia ) || matchUserAgent( 'Edg' ),
// Google Chrome (Not accurate)
isChrome = !! window.chrome && matchUserAgent( 'Chrome' ) && ! ( isEdge || isOpera ),
// Blink engine
isBlink = matchUserAgent( 'Chrome' ) && !! window.CSS,
// Apple Webkit engine
isAppleWebkit = matchUserAgent( 'AppleWebKit' ) && ! isBlink,
environment = {
appleWebkit: isAppleWebkit,
blink: isBlink,
chrome: isChrome,
edge: isEdge,
firefox: isFirefox,
ie: isIE,
mac: matchUserAgent( 'Macintosh' ),
opera: isOpera,
safari: isSafari,
webkit: matchUserAgent( 'AppleWebKit' ),
};
export default environment;

View File

@@ -0,0 +1,64 @@
export default class Helpers {
softDeprecated( name, version, replacement ) {
if ( elementorCommon.config.isDebug ) {
this.deprecatedMessage( 'soft', name, version, replacement );
}
}
hardDeprecated( name, version, replacement ) {
this.deprecatedMessage( 'hard', name, version, replacement );
}
deprecatedMessage( type, name, version, replacement ) {
let message = `\`${ name }\` is ${ type } deprecated since ${ version }`;
if ( replacement ) {
message += ` - Use \`${ replacement }\` instead`;
}
this.consoleWarn( message );
}
consoleWarn( ...args ) {
const style = `font-size: 12px; background-image: url("${ elementorCommon.config.urls.assets }images/logo-icon.png"); background-repeat: no-repeat; background-size: contain;`;
args.unshift( '%c %c', style, '' );
console.warn( ...args ); // eslint-disable-line no-console
}
consoleError( message ) {
// TODO: function is part of $e.
// Show an error if devTools is available.
if ( $e.devTools ) {
$e.devTools.log.error( message );
}
// If not a 'Hook-Break' then show error.
if ( ! ( message instanceof $e.modules.HookBreak ) ) {
// eslint-disable-next-line no-console
console.error( message );
}
}
deprecatedMethod( methodName, version, replacement ) {
this.deprecatedMessage( 'hard', methodName, version, replacement );
// This itself is deprecated.
this.softDeprecated( 'elementorCommon.helpers.deprecatedMethod', '2.8.0', 'elementorCommon.helpers.softDeprecated || elementorCommon.helpers.hardDeprecated' );
}
cloneObject( object ) {
return JSON.parse( JSON.stringify( object ) );
}
upperCaseWords( string ) {
return ( string + '' ).replace( /^(.)|\s+(.)/g, function( $1 ) {
return $1.toUpperCase();
} );
}
getUniqueId() {
return Math.random().toString( 16 ).substr( 2, 7 );
}
}

View File

@@ -0,0 +1,89 @@
export default class extends elementorModules.Module {
get( key, options ) {
options = options || {};
let storage;
try {
storage = options.session ? sessionStorage : localStorage;
} catch ( e ) {
return key ? undefined : {};
}
let elementorStorage = storage.getItem( 'elementor' );
if ( elementorStorage ) {
elementorStorage = JSON.parse( elementorStorage );
} else {
elementorStorage = {};
}
if ( ! elementorStorage.__expiration ) {
elementorStorage.__expiration = {};
}
const expiration = elementorStorage.__expiration;
let expirationToCheck = [];
if ( key ) {
if ( expiration[ key ] ) {
expirationToCheck = [ key ];
}
} else {
expirationToCheck = Object.keys( expiration );
}
let entryExpired = false;
expirationToCheck.forEach( ( expirationKey ) => {
if ( new Date( expiration[ expirationKey ] ) < new Date() ) {
delete elementorStorage[ expirationKey ];
delete expiration[ expirationKey ];
entryExpired = true;
}
} );
if ( entryExpired ) {
this.save( elementorStorage, options.session );
}
if ( key ) {
return elementorStorage[ key ];
}
return elementorStorage;
}
set( key, value, options ) {
options = options || {};
const elementorStorage = this.get( null, options );
elementorStorage[ key ] = value;
if ( options.lifetimeInSeconds ) {
const date = new Date();
date.setTime( date.getTime() + ( options.lifetimeInSeconds * 1000 ) );
elementorStorage.__expiration[ key ] = date.getTime();
}
this.save( elementorStorage, options.session );
}
save( object, session ) {
let storage;
try {
storage = session ? sessionStorage : localStorage;
} catch ( e ) {
return;
}
storage.setItem( 'elementor', JSON.stringify( object ) );
}
}

View File

@@ -0,0 +1,39 @@
export default class extends Marionette.LayoutView {
className() {
return 'elementor-templates-modal__header';
}
getTemplate() {
return '#tmpl-elementor-templates-modal__header';
}
regions() {
return {
logoArea: '.elementor-templates-modal__header__logo-area',
tools: '#elementor-template-library-header-tools',
menuArea: '.elementor-templates-modal__header__menu-area',
};
}
ui() {
return {
closeModal: '.elementor-templates-modal__header__close',
};
}
events() {
return {
'click @ui.closeModal': 'onCloseModalClick',
};
}
templateHelpers() {
return {
closeType: this.getOption( 'closeType' ),
};
}
onCloseModalClick() {
this._parent._parent._parent.hideModal();
}
}

View File

@@ -0,0 +1,108 @@
import HeaderView from './header';
import LogoView from './logo';
import LoadingView from './loading';
export default class extends Marionette.LayoutView {
el() {
return this.getModal().getElements( 'widget' );
}
regions() {
return {
modalHeader: '.dialog-header',
modalContent: '.dialog-lightbox-content',
modalLoading: '.dialog-lightbox-loading',
};
}
initialize() {
this.modalHeader.show( new HeaderView( this.getHeaderOptions() ) );
}
getModal() {
if ( ! this.modal ) {
this.initModal();
}
return this.modal;
}
initModal() {
const modalOptions = {
className: 'elementor-templates-modal',
closeButton: false,
draggable: false,
hide: {
onOutsideClick: false,
onEscKeyPress: false,
},
};
jQuery.extend( true, modalOptions, this.getModalOptions() );
this.modal = elementorCommon.dialogsManager.createWidget( 'lightbox', modalOptions );
this.modal.getElements( 'message' ).append( this.modal.addElement( 'content' ), this.modal.addElement( 'loading' ) );
if ( modalOptions.draggable ) {
this.draggableModal();
}
}
showModal() {
this.getModal().show();
}
hideModal() {
this.getModal().hide();
}
draggableModal() {
const $modalWidgetContent = this.getModal().getElements( 'widgetContent' );
$modalWidgetContent.draggable( {
containment: 'parent',
stop: () => {
$modalWidgetContent.height( '' );
},
} );
$modalWidgetContent.css( 'position', 'absolute' );
}
getModalOptions() {
return {};
}
getLogoOptions() {
return {};
}
getHeaderOptions() {
return {
closeType: 'normal',
};
}
getHeaderView() {
return this.modalHeader.currentView;
}
showLoadingView() {
this.modalLoading.show( new LoadingView() );
this.modalLoading.$el.show();
this.modalContent.$el.hide();
}
hideLoadingView() {
this.modalContent.$el.show();
this.modalLoading.$el.hide();
}
showLogo() {
this.getHeaderView().logoArea.show( new LogoView( this.getLogoOptions() ) );
}
}

View File

@@ -0,0 +1,9 @@
export default class extends Marionette.ItemView {
id() {
return 'elementor-template-library-loading';
}
getTemplate() {
return '#tmpl-elementor-template-library-loading';
}
}

View File

@@ -0,0 +1,29 @@
export default class extends Marionette.ItemView {
getTemplate() {
return '#tmpl-elementor-templates-modal__header__logo';
}
className() {
return 'elementor-templates-modal__header__logo';
}
events() {
return {
click: 'onClick',
};
}
templateHelpers() {
return {
title: this.getOption( 'title' ),
};
}
onClick() {
const clickCallback = this.getOption( 'click' );
if ( clickCallback ) {
clickCallback();
}
}
}

View File

@@ -0,0 +1,111 @@
//
// Tipsy (tooltip)
//
.tipsy {
font-size: 10px;
position: absolute;
padding: 5px;
z-index: 100000;
}
.tipsy-inner {
background-color: #000;
color: #FFF;
font-weight: 500;
max-width: 200px;
padding: 5px 12px;
text-align: center;
border-radius: 3px;
box-shadow: 0 0 5px 0 rgba(0,0,0,0.5);
}
.tipsy-arrow {
position: absolute;
width: 0;
height: 0;
line-height: 0;
border: 5px dashed #000
}
.tipsy-arrow-n {
border-bottom-color: #000
}
.tipsy-arrow-s {
border-top-color: #000
}
.tipsy-arrow-e {
border-left-color: #000
}
.tipsy-arrow-w {
border-right-color: #000
}
.tipsy-n .tipsy-arrow, .tipsy-nw .tipsy-arrow {
border-left-color: transparent;
border-right-color: transparent;
top: 0;
border-bottom-style: solid;
border-top: none
}
.tipsy-n .tipsy-arrow {
left: 50%;
margin-left: -5px
}
.tipsy-nw .tipsy-arrow {
left: 10px
}
.tipsy-ne .tipsy-arrow {
top: 0;
right: 10px;
border-bottom-style: solid;
border-top: none;
border-left-color: transparent;
border-right-color: transparent
}
.tipsy-s .tipsy-arrow, .tipsy-se .tipsy-arrow, .tipsy-sw .tipsy-arrow {
bottom: 0;
border-top-style: solid;
border-bottom: none;
border-left-color: transparent;
border-right-color: transparent
}
.tipsy-s .tipsy-arrow {
left: 50%;
margin-left: -5px
}
.tipsy-sw .tipsy-arrow {
left: 10px
}
.tipsy-se .tipsy-arrow {
right: 10px
}
.tipsy-e .tipsy-arrow, .tipsy-w .tipsy-arrow {
top: 50%;
margin-top: -5px;
border-top-color: transparent;
border-bottom-color: transparent
}
.tipsy-e .tipsy-arrow {
right: 0;
border-left-style: solid;
border-right: none
}
.tipsy-w .tipsy-arrow {
left: 0;
border-right-style: solid;
border-left: none
}

View File

@@ -0,0 +1,4 @@
@import "../../../../assets/dev/scss/global/global";
@import "../../modules/finder/assets/scss/finder";
@import "tipsy";

View File

@@ -0,0 +1,229 @@
export default class extends elementorModules.Module {
getDefaultSettings() {
return {
ajaxParams: {
type: 'POST',
url: elementorCommon.config.ajax.url,
data: {},
dataType: 'json',
},
actionPrefix: 'elementor_',
};
}
constructor( ...args ) {
super( ...args );
this.requests = {};
this.cache = {};
this.initRequestConstants();
this.debounceSendBatch = _.debounce( this.sendBatch.bind( this ), 500 );
}
initRequestConstants() {
this.requestConstants = {
_nonce: this.getSettings( 'nonce' ),
};
}
addRequestConstant( key, value ) {
this.requestConstants[ key ] = value;
}
getCacheKey( request ) {
return JSON.stringify( {
unique_id: request.unique_id,
data: request.data,
} );
}
loadObjects( options ) {
let dataCollection = {};
const deferredArray = [];
if ( options.before ) {
options.before();
}
options.ids.forEach( ( objectId ) => {
deferredArray.push(
this.load( {
action: options.action,
unique_id: options.data.unique_id + objectId,
data: jQuery.extend( { id: objectId }, options.data ),
} )
.done( ( data ) => dataCollection = jQuery.extend( dataCollection, data ) )
);
} );
jQuery.when.apply( jQuery, deferredArray ).done( () => options.success( dataCollection ) );
}
load( request, immediately ) {
if ( ! request.unique_id ) {
request.unique_id = request.action;
}
if ( request.before ) {
request.before();
}
let deferred;
const cacheKey = this.getCacheKey( request );
if ( _.has( this.cache, cacheKey ) ) {
deferred = jQuery.Deferred()
.done( request.success )
.resolve( this.cache[ cacheKey ] );
} else {
deferred = this.addRequest( request.action, {
data: request.data,
unique_id: request.unique_id,
success: ( data ) => this.cache[ cacheKey ] = data,
}, immediately ).done( request.success );
}
return deferred;
}
addRequest( action, options, immediately ) {
options = options || {};
if ( ! options.unique_id ) {
options.unique_id = action;
}
options.deferred = jQuery.Deferred().done( options.success ).fail( options.error ).always( options.complete );
const request = {
action: action,
options: options,
};
if ( immediately ) {
const requests = {};
requests[ options.unique_id ] = request;
options.deferred.jqXhr = this.sendBatch( requests );
} else {
this.requests[ options.unique_id ] = request;
this.debounceSendBatch();
}
return options.deferred;
}
sendBatch( requests ) {
const actions = {};
if ( ! requests ) {
requests = this.requests;
// Empty for next batch.
this.requests = {};
}
Object.entries( requests ).forEach( ( [ id, request ] ) => actions[ id ] = {
action: request.action,
data: request.options.data,
} );
return this.send( 'ajax', {
data: {
actions: JSON.stringify( actions ),
},
success: ( data ) => {
Object.entries( data.responses ).forEach( ( [ id, response ] ) => {
const options = requests[ id ].options;
if ( options ) {
if ( response.success ) {
options.deferred.resolve( response.data );
} else if ( ! response.success ) {
options.deferred.reject( response.data );
}
}
} );
},
error: ( data ) =>
Object.values( requests ).forEach( ( args ) => {
if ( args.options ) {
args.options.deferred.reject( data );
}
} ),
} );
}
prepareSend( action, options ) {
const settings = this.getSettings(),
ajaxParams = elementorCommon.helpers.cloneObject( settings.ajaxParams );
options = options || {};
action = settings.actionPrefix + action;
jQuery.extend( ajaxParams, options );
const requestConstants = elementorCommon.helpers.cloneObject( this.requestConstants );
requestConstants.action = action;
const isFormData = ajaxParams.data instanceof FormData;
Object.entries( requestConstants ).forEach( ( [ key, value ] ) => {
if ( isFormData ) {
ajaxParams.data.append( key, value );
} else {
ajaxParams.data[ key ] = value;
}
} );
const successCallback = ajaxParams.success,
errorCallback = ajaxParams.error;
if ( successCallback || errorCallback ) {
ajaxParams.success = ( response ) => {
if ( response.success && successCallback ) {
successCallback( response.data );
}
if ( ( ! response.success ) && errorCallback ) {
errorCallback( response.data );
}
};
if ( errorCallback ) {
ajaxParams.error = ( data ) => errorCallback( data );
} else {
ajaxParams.error = ( xmlHttpRequest ) => {
if ( xmlHttpRequest.readyState || 'abort' !== xmlHttpRequest.statusText ) {
this.trigger( 'request:unhandledError', xmlHttpRequest );
}
};
}
}
return ajaxParams;
}
send( action, options ) {
return jQuery.ajax( this.prepareSend( action, options ) );
}
addRequestCache( request, data ) {
const cacheKey = this.getCacheKey( request );
this.cache[ cacheKey ] = data;
}
invalidateCache( request ) {
const cacheKey = this.getCacheKey( request );
delete this.cache[ cacheKey ];
}
}

View File

@@ -0,0 +1,320 @@
<?php
namespace Elementor\Core\Common\Modules\Ajax;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Utils\Exceptions;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor ajax manager.
*
* Elementor ajax manager handler class is responsible for handling Elementor
* ajax requests, ajax responses and registering actions applied on them.
*
* @since 2.0.0
*/
class Module extends BaseModule {
const NONCE_KEY = 'elementor_ajax';
/**
* Ajax actions.
*
* Holds all the register ajax action.
*
* @since 2.0.0
* @access private
*
* @var array
*/
private $ajax_actions = [];
/**
* Ajax requests.
*
* Holds all the register ajax requests.
*
* @since 2.0.0
* @access private
*
* @var array
*/
private $requests = [];
/**
* Ajax response data.
*
* Holds all the response data for all the ajax requests.
*
* @since 2.0.0
* @access private
*
* @var array
*/
private $response_data = [];
/**
* Current ajax action ID.
*
* Holds all the ID for the current ajax action.
*
* @since 2.0.0
* @access private
*
* @var string|null
*/
private $current_action_id = null;
/**
* Ajax manager constructor.
*
* Initializing Elementor ajax manager.
*
* @since 2.0.0
* @access public
*/
public function __construct() {
add_action( 'wp_ajax_elementor_ajax', [ $this, 'handle_ajax_request' ] );
}
/**
* Get module name.
*
* Retrieve the module name.
*
* @since 1.7.0
* @access public
*
* @return string Module name.
*/
public function get_name() {
return 'ajax';
}
/**
* Register ajax action.
*
* Add new actions for a specific ajax request and the callback function to
* be handle the response.
*
* @since 2.0.0
* @access public
*
* @param string $tag Ajax request name/tag.
* @param callable $callback The callback function.
*/
public function register_ajax_action( $tag, $callback ) {
if ( ! did_action( 'elementor/ajax/register_actions' ) ) {
_doing_it_wrong( __METHOD__, esc_html( sprintf( 'Use `%s` hook to register ajax action.', 'elementor/ajax/register_actions' ) ), '2.0.0' );
}
$this->ajax_actions[ $tag ] = compact( 'tag', 'callback' );
}
/**
* Handle ajax request.
*
* Verify ajax nonce, and run all the registered actions for this request.
*
* Fired by `wp_ajax_elementor_ajax` action.
*
* @since 2.0.0
* @access public
*/
public function handle_ajax_request() {
if ( ! $this->verify_request_nonce() ) {
$this->add_response_data( false, __( 'Token Expired.', 'elementor' ) )
->send_error( Exceptions::UNAUTHORIZED );
}
$editor_post_id = 0;
if ( ! empty( $_REQUEST['editor_post_id'] ) ) {
$editor_post_id = absint( $_REQUEST['editor_post_id'] );
Plugin::$instance->db->switch_to_post( $editor_post_id );
}
/**
* Register ajax actions.
*
* Fires when an ajax request is received and verified.
*
* Used to register new ajax action handles.
*
* @since 2.0.0
*
* @param self $this An instance of ajax manager.
*/
do_action( 'elementor/ajax/register_actions', $this );
$this->requests = json_decode( stripslashes( $_REQUEST['actions'] ), true );
foreach ( $this->requests as $id => $action_data ) {
$this->current_action_id = $id;
if ( ! isset( $this->ajax_actions[ $action_data['action'] ] ) ) {
$this->add_response_data( false, __( 'Action not found.', 'elementor' ), Exceptions::BAD_REQUEST );
continue;
}
if ( $editor_post_id ) {
$action_data['data']['editor_post_id'] = $editor_post_id;
}
try {
$results = call_user_func( $this->ajax_actions[ $action_data['action'] ]['callback'], $action_data['data'], $this );
if ( false === $results ) {
$this->add_response_data( false );
} else {
$this->add_response_data( true, $results );
}
} catch ( \Exception $e ) {
$this->add_response_data( false, $e->getMessage(), $e->getCode() );
}
}
$this->current_action_id = null;
$this->send_success();
}
/**
* Get current action data.
*
* Retrieve the data for the current ajax request.
*
* @since 2.0.1
* @access public
*
* @return bool|mixed Ajax request data if action exist, False otherwise.
*/
public function get_current_action_data() {
if ( ! $this->current_action_id ) {
return false;
}
return $this->requests[ $this->current_action_id ];
}
/**
* Create nonce.
*
* Creates a cryptographic token to
* give the user an access to Elementor ajax actions.
*
* @since 2.3.0
* @access public
*
* @return string The nonce token.
*/
public function create_nonce() {
return wp_create_nonce( self::NONCE_KEY );
}
/**
* Verify request nonce.
*
* Whether the request nonce verified or not.
*
* @since 2.3.0
* @access public
*
* @return bool True if request nonce verified, False otherwise.
*/
public function verify_request_nonce() {
return ! empty( $_REQUEST['_nonce'] ) && wp_verify_nonce( $_REQUEST['_nonce'], self::NONCE_KEY );
}
protected function get_init_settings() {
return [
'url' => admin_url( 'admin-ajax.php' ),
'nonce' => $this->create_nonce(),
];
}
/**
* Ajax success response.
*
* Send a JSON response data back to the ajax request, indicating success.
*
* @since 2.0.0
* @access protected
*/
private function send_success() {
$response = [
'success' => true,
'data' => [
'responses' => $this->response_data,
],
];
$json = wp_json_encode( $response );
while ( ob_get_status() ) {
ob_end_clean();
}
if ( function_exists( 'gzencode' ) ) {
$response = gzencode( $json );
header( 'Content-Type: application/json; charset=utf-8' );
header( 'Content-Encoding: gzip' );
header( 'Content-Length: ' . strlen( $response ) );
echo $response;
} else {
echo $json;
}
wp_die( '', '', [ 'response' => null ] );
}
/**
* Ajax failure response.
*
* Send a JSON response data back to the ajax request, indicating failure.
*
* @since 2.0.0
* @access protected
*
* @param null $code
*/
private function send_error( $code = null ) {
wp_send_json_error( [
'responses' => $this->response_data,
], $code );
}
/**
* Add response data.
*
* Add new response data to the array of all the ajax requests.
*
* @since 2.0.0
* @access protected
*
* @param bool $success True if the requests returned successfully, False
* otherwise.
* @param mixed $data Optional. Response data. Default is null.
*
* @param int $code Optional. Response code. Default is 200.
*
* @return Module An instance of ajax manager.
*/
private function add_response_data( $success, $data = null, $code = 200 ) {
$this->response_data[ $this->current_action_id ] = [
'success' => $success,
'code' => $code,
'data' => $data,
];
return $this;
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Elementor\Core\Common\Modules\Connect;
use Elementor\Plugin;
use Elementor\Settings;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Admin {
const PAGE_ID = 'elementor-connect';
public static $url = '';
/**
* @since 2.3.0
* @access public
*/
public function register_admin_menu() {
$submenu_page = add_submenu_page(
Settings::PAGE_ID,
__( 'Connect', 'elementor' ),
__( 'Connect', 'elementor' ),
'edit_posts',
self::PAGE_ID,
[ $this, 'render_page' ]
);
add_action( 'load-' . $submenu_page, [ $this, 'on_load_page' ] );
}
/**
* @since 2.3.0
* @access public
*/
public function hide_menu_item() {
remove_submenu_page( Settings::PAGE_ID, self::PAGE_ID );
}
/**
* @since 2.3.0
* @access public
*/
public function on_load_page() {
if ( isset( $_GET['action'], $_GET['app'] ) ) {
$manager = Plugin::$instance->common->get_component( 'connect' );
$app_slug = $_GET['app'];
$app = $manager->get_app( $app_slug );
$nonce_action = $_GET['app'] . $_GET['action'];
if ( ! $app ) {
wp_die( 'Unknown app: ' . esc_attr( $app_slug ) );
}
if ( empty( $_GET['nonce'] ) || ! wp_verify_nonce( $_GET['nonce'], $nonce_action ) ) {
wp_die( 'Invalid Nonce', 'Invalid Nonce', [
'back_link' => true,
] );
}
$method = 'action_' . $_GET['action'];
if ( method_exists( $app, $method ) ) {
call_user_func( [ $app, $method ] );
}
}
}
/**
* @since 2.3.0
* @access public
*/
public function render_page() {
$apps = Plugin::$instance->common->get_component( 'connect' )->get_apps();
?>
<style>
.elementor-connect-app-wrapper{
margin-bottom: 50px;
overflow: hidden;
}
</style>
<div class="wrap">
<?php
/** @var \Elementor\Core\Common\Modules\Connect\Apps\Base_App $app */
foreach ( $apps as $app ) {
echo '<div class="elementor-connect-app-wrapper">';
$app->render_admin_widget();
echo '</div>';
}
?>
</div><!-- /.wrap -->
<?php
}
/**
* @since 2.3.0
* @access public
*/
public function __construct() {
self::$url = admin_url( 'admin.php?page=' . self::PAGE_ID );
add_action( 'admin_menu', [ $this, 'register_admin_menu' ], 206 );
add_action( 'admin_head', [ $this, 'hide_menu_item' ] );
}
}

View File

@@ -0,0 +1,638 @@
<?php
namespace Elementor\Core\Common\Modules\Connect\Apps;
use Elementor\Core\Admin\Admin_Notices;
use Elementor\Core\Common\Modules\Connect\Admin;
use Elementor\Plugin;
use Elementor\Tracker;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
abstract class Base_App {
const OPTION_NAME_PREFIX = 'elementor_connect_';
const SITE_URL = 'https://my.elementor.com/connect/v1';
const API_URL = 'https://my.elementor.com/api/connect/v1';
protected $data = [];
protected $auth_mode = '';
/**
* @since 2.3.0
* @access protected
* @abstract
* TODO: make it public.
*/
abstract protected function get_slug();
/**
* @since 2.8.0
* @access public
* TODO: make it abstract.
*/
public function get_title() {
return $this->get_slug();
}
/**
* @since 2.3.0
* @access protected
* @abstract
*/
abstract protected function update_settings();
/**
* @since 2.3.0
* @access public
* @static
*/
public static function get_class_name() {
return get_called_class();
}
/**
* @access public
* @abstract
*/
public function render_admin_widget() {
echo '<h2>' . $this->get_title() . '</h2>';
if ( $this->is_connected() ) {
$remote_user = $this->get( 'user' );
$title = sprintf( __( 'Connected as %s', 'elementor' ), '<strong>' . $remote_user->email . '</strong>' );
$label = __( 'Disconnect', 'elementor' );
$url = $this->get_admin_url( 'disconnect' );
$attr = '';
echo sprintf( '%s <a %s href="%s">%s</a>', $title, $attr, esc_attr( $url ), esc_html( $label ) );
} else {
echo 'Not Connected';
}
echo '<hr>';
$this->print_app_info();
if ( current_user_can( 'manage_options' ) ) {
printf( '<div><a href="%s">%s</a></div>', $this->get_admin_url( 'reset' ), __( 'Reset Data', 'elementor' ) );
}
echo '<hr>';
}
/**
* @since 2.3.0
* @access protected
*/
protected function get_option_name() {
return static::OPTION_NAME_PREFIX . $this->get_slug();
}
/**
* @since 2.3.0
* @access public
*/
public function admin_notice() {
$notices = $this->get( 'notices' );
if ( ! $notices ) {
return;
}
$this->print_notices( $notices );
$this->delete( 'notices' );
}
public function get_app_token_from_cli_token( $cli_token ) {
$response = $this->request( 'get_app_token_from_cli_token', [
'cli_token' => $cli_token,
] );
if ( is_wp_error( $response ) ) {
wp_die( $response, $response->get_error_message() );
}
// Use state as usual.
$_REQUEST['state'] = $this->get( 'state' );
$_REQUEST['code'] = $response->code;
}
/**
* @since 2.3.0
* @access public
*/
public function action_authorize() {
if ( $this->is_connected() ) {
$this->add_notice( __( 'Already connected.', 'elementor' ), 'info' );
$this->redirect_to_admin_page();
return;
}
$this->set_client_id();
$this->set_request_state();
$this->redirect_to_remote_authorize_url();
}
public function action_reset() {
delete_user_option( get_current_user_id(), 'elementor_connect_common_data' );
if ( current_user_can( 'manage_options' ) ) {
delete_option( 'elementor_connect_site_key' );
delete_option( 'elementor_remote_info_library' );
}
$this->redirect_to_admin_page();
}
/**
* @since 2.3.0
* @access public
*/
public function action_get_token() {
if ( $this->is_connected() ) {
$this->redirect_to_admin_page();
}
if ( empty( $_REQUEST['state'] ) || $_REQUEST['state'] !== $this->get( 'state' ) ) {
$this->add_notice( 'Get Token: Invalid Request.', 'error' );
$this->redirect_to_admin_page();
}
$response = $this->request( 'get_token', [
'grant_type' => 'authorization_code',
'code' => $_REQUEST['code'],
'redirect_uri' => rawurlencode( $this->get_admin_url( 'get_token' ) ),
'client_id' => $this->get( 'client_id' ),
] );
if ( is_wp_error( $response ) ) {
$notice = 'Cannot Get Token:' . $response->get_error_message();
$this->add_notice( $notice, 'error' );
$this->redirect_to_admin_page();
}
if ( ! empty( $response->data_share_opted_in ) && current_user_can( 'manage_options' ) ) {
Tracker::set_opt_in( true );
}
$this->delete( 'state' );
$this->set( (array) $response );
$this->after_connect();
// Add the notice *after* the method `after_connect`, so an app can redirect without the notice.
$this->add_notice( __( 'Connected Successfully.', 'elementor' ) );
$this->redirect_to_admin_page();
}
/**
* @since 2.3.0
* @access public
*/
public function action_disconnect() {
if ( $this->is_connected() ) {
$this->disconnect();
$this->add_notice( __( 'Disconnected Successfully.', 'elementor' ) );
}
$this->redirect_to_admin_page();
}
/**
* @since 2.8.0
* @access public
*/
public function action_reconnect() {
$this->disconnect();
$this->action_authorize();
}
/**
* @since 2.3.0
* @access public
*/
public function get_admin_url( $action, $params = [] ) {
$params = [
'app' => $this->get_slug(),
'action' => $action,
'nonce' => wp_create_nonce( $this->get_slug() . $action ),
] + $params;
// Encode base url, the encode is limited to 64 chars.
$admin_url = \Requests_IDNAEncoder::encode( get_admin_url() );
$admin_url .= 'admin.php?page=' . Admin::PAGE_ID;
return add_query_arg( $params, $admin_url );
}
/**
* @since 2.3.0
* @access public
*/
public function is_connected() {
return (bool) $this->get( 'access_token' );
}
/**
* @since 2.3.0
* @access protected
*/
protected function init() {}
/**
* @since 2.3.0
* @access protected
*/
protected function init_data() {}
/**
* @since 2.3.0
* @access protected
*/
protected function after_connect() {}
/**
* @since 2.3.0
* @access public
*/
public function get( $key, $default = null ) {
$this->init_data();
return isset( $this->data[ $key ] ) ? $this->data[ $key ] : $default;
}
/**
* @since 2.3.0
* @access protected
*/
protected function set( $key, $value = null ) {
$this->init_data();
if ( is_array( $key ) ) {
$this->data = array_replace_recursive( $this->data, $key );
} else {
$this->data[ $key ] = $value;
}
$this->update_settings();
}
/**
* @since 2.3.0
* @access protected
*/
protected function delete( $key = null ) {
$this->init_data();
if ( $key ) {
unset( $this->data[ $key ] );
} else {
$this->data = [];
}
$this->update_settings();
}
/**
* @since 2.3.0
* @access protected
*/
protected function add( $key, $value, $default = '' ) {
$new_value = $this->get( $key, $default );
if ( is_array( $new_value ) ) {
$new_value[] = $value;
} elseif ( is_string( $new_value ) ) {
$new_value .= $value;
} elseif ( is_numeric( $new_value ) ) {
$new_value += $value;
}
$this->set( $key, $new_value );
}
/**
* @since 2.3.0
* @access protected
*/
protected function add_notice( $content, $type = 'success' ) {
$this->add( 'notices', compact( 'content', 'type' ), [] );
}
/**
* @since 2.3.0
* @access protected
*/
protected function request( $action, $request_body = [], $as_array = false ) {
$request_body = [
'app' => $this->get_slug(),
'access_token' => $this->get( 'access_token' ),
'client_id' => $this->get( 'client_id' ),
'local_id' => get_current_user_id(),
'site_key' => $this->get_site_key(),
'home_url' => trailingslashit( home_url() ),
] + $request_body;
$headers = [];
if ( $this->is_connected() ) {
$headers['X-Elementor-Signature'] = hash_hmac( 'sha256', wp_json_encode( $request_body, JSON_NUMERIC_CHECK ), $this->get( 'access_token_secret' ) );
}
$response = wp_remote_post( $this->get_api_url() . '/' . $action, [
'body' => $request_body,
'headers' => $headers,
'timeout' => 25,
] );
if ( is_wp_error( $response ) ) {
wp_die( $response, [
'back_link' => true,
] );
}
$body = wp_remote_retrieve_body( $response );
$response_code = (int) wp_remote_retrieve_response_code( $response );
if ( ! $response_code ) {
return new \WP_Error( 500, 'No Response' );
}
// Server sent a success message without content.
if ( 'null' === $body ) {
$body = true;
}
$body = json_decode( $body, $as_array );
if ( false === $body ) {
return new \WP_Error( 422, 'Wrong Server Response' );
}
if ( 200 !== $response_code ) {
// In case $as_array = true.
$body = (object) $body;
$message = isset( $body->message ) ? $body->message : wp_remote_retrieve_response_message( $response );
$code = (int) ( isset( $body->code ) ? $body->code : $response_code );
if ( 401 === $code ) {
$this->delete();
$this->action_authorize();
}
return new \WP_Error( $code, $message );
}
return $body;
}
/**
* @since 2.3.0
* @access protected
*/
protected function get_api_url() {
return static::API_URL . '/' . $this->get_slug();
}
/**
* @since 2.3.0
* @access protected
*/
protected function get_remote_site_url() {
return static::SITE_URL . '/' . $this->get_slug();
}
/**
* @since 2.3.0
* @access protected
*/
protected function get_remote_authorize_url() {
$redirect_uri = $this->get_auth_redirect_uri();
$url = add_query_arg( [
'action' => 'authorize',
'response_type' => 'code',
'client_id' => $this->get( 'client_id' ),
'auth_secret' => $this->get( 'auth_secret' ),
'state' => $this->get( 'state' ),
'redirect_uri' => rawurlencode( $redirect_uri ),
'may_share_data' => current_user_can( 'manage_options' ) && ! Tracker::is_allow_track(),
'reconnect_nonce' => wp_create_nonce( $this->get_slug() . 'reconnect' ),
], $this->get_remote_site_url() );
return $url;
}
/**
* @since 2.3.0
* @access protected
*/
protected function redirect_to_admin_page( $url = '' ) {
if ( ! $url ) {
$url = Admin::$url;
}
switch ( $this->auth_mode ) {
case 'popup':
$this->print_popup_close_script( $url );
break;
case 'cli':
$this->admin_notice();
die;
default:
wp_safe_redirect( $url );
die;
}
}
/**
* @since 2.3.0
* @access protected
*/
protected function set_client_id() {
if ( $this->get( 'client_id' ) ) {
return;
}
$response = $this->request( 'get_client_id' );
if ( is_wp_error( $response ) ) {
wp_die( $response, $response->get_error_message() );
}
$this->set( 'client_id', $response->client_id );
$this->set( 'auth_secret', $response->auth_secret );
}
/**
* @since 2.3.0
* @access protected
*/
protected function set_request_state() {
$this->set( 'state', wp_generate_password( 12, false ) );
}
/**
* @since 2.3.0
* @access protected
*/
protected function print_popup_close_script( $url ) {
?>
<script>
if ( opener && opener !== window ) {
opener.jQuery( 'body' ).trigger( 'elementor/connect/success/<?php echo esc_attr( $_REQUEST['callback_id'] ); ?>' );
window.close();
opener.focus();
} else {
location = '<?php echo $url; ?>';
}
</script>
<?php
die;
}
/**
* @since 2.3.0
* @access protected
*/
protected function disconnect() {
if ( $this->is_connected() ) {
// Try update the server, but not needed to handle errors.
$this->request( 'disconnect' );
}
$this->delete();
}
/**
* @since 2.3.0
* @access protected
*/
protected function get_site_key() {
$site_key = get_option( 'elementor_connect_site_key' );
if ( ! $site_key ) {
$site_key = md5( uniqid( wp_generate_password() ) );
update_option( 'elementor_connect_site_key', $site_key );
}
return $site_key;
}
protected function redirect_to_remote_authorize_url() {
switch ( $this->auth_mode ) {
case 'cli':
$this->get_app_token_from_cli_token( $_REQUEST['token'] );
return;
default:
wp_redirect( $this->get_remote_authorize_url() );
die;
}
}
protected function get_auth_redirect_uri() {
$redirect_uri = $this->get_admin_url( 'get_token' );
switch ( $this->auth_mode ) {
case 'popup':
$redirect_uri = add_query_arg( [
'mode' => 'popup',
'callback_id' => esc_attr( $_REQUEST['callback_id'] ),
], $redirect_uri );
break;
}
return $redirect_uri;
}
protected function print_notices( $notices ) {
switch ( $this->auth_mode ) {
case 'cli':
foreach ( $notices as $notice ) {
printf( '[%s] %s', $notice['type'], $notice['content'] );
}
break;
default:
/**
* @var Admin_Notices $admin_notices
*/
$admin_notices = Plugin::$instance->admin->get_component( 'admin-notices' );
foreach ( $notices as $notice ) {
$options = [
'description' => wp_kses_post( wpautop( $notice['content'] ) ),
'type' => $notice['type'],
'icon' => false,
];
$admin_notices->print_admin_notice( $options );
}
}
}
protected function get_app_info() {
return [];
}
protected function print_app_info() {
$app_info = $this->get_app_info();
foreach ( $app_info as $key => $item ) {
if ( $item['value'] ) {
$status = 'Exist';
$color = 'green';
} else {
$status = 'Empty';
$color = 'red';
}
printf( '%s: <strong style="color:%s">%s</strong><br>', $item['label'], $color, $status );
}
}
/**
* @since 2.3.0
* @access public
*/
public function __construct() {
add_action( 'admin_notices', [ $this, 'admin_notice' ] );
if ( isset( $_REQUEST['mode'] ) ) { // phpcs:ignore -- nonce validation is not require here.
$allowed_auth_modes = [
'popup',
];
if ( defined( 'WP_CLI' ) && WP_CLI ) {
$allowed_auth_modes[] = 'cli';
}
$mode = $_REQUEST['mode']; // phpcs:ignore -- nonce validation is not require here.
if ( in_array( $mode, $allowed_auth_modes, true ) ) {
$this->auth_mode = $mode;
}
}
/**
* Allow extended apps to customize the __construct without call parent::__construct.
*/
$this->init();
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Elementor\Core\Common\Modules\Connect\Apps;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
abstract class Base_User_App extends Base_App {
/**
* @since 2.3.0
* @access protected
*/
protected function update_settings() {
update_user_option( get_current_user_id(), $this->get_option_name(), $this->data );
}
/**
* @since 2.3.0
* @access protected
*/
protected function init_data() {
$this->data = get_user_option( $this->get_option_name() );
if ( ! $this->data ) {
$this->data = [];
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Elementor\Core\Common\Modules\Connect\Apps;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
abstract class Common_App extends Base_User_App {
protected static $common_data = null;
/**
* @since 2.3.0
* @access public
*/
public function get_option_name() {
return static::OPTION_NAME_PREFIX . 'common_data';
}
/**
* @since 2.3.0
* @access protected
*/
protected function init_data() {
if ( is_null( self::$common_data ) ) {
self::$common_data = get_user_option( static::get_option_name() );
if ( ! self::$common_data ) {
self::$common_data = [];
};
}
$this->data = & self::$common_data;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Elementor\Core\Common\Modules\Connect\Apps;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Connect extends Common_App {
public function get_title() {
return __( 'Connect', 'elementor' );
}
/**
* @since 2.3.0
* @access public
*/
protected function get_slug() {
return 'connect';
}
/**
* @since 2.3.0
* @access public
*/
public function render_admin_widget() {}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Elementor\Core\Common\Modules\Connect\Apps;
use Elementor\User;
use Elementor\Plugin;
use Elementor\Core\Common\Modules\Connect\Module as ConnectModule;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Library extends Common_App {
public function get_title() {
return __( 'Library', 'elementor' );
}
/**
* @since 2.3.0
* @access protected
*/
protected function get_slug() {
return 'library';
}
public function get_template_content( $id ) {
if ( ! $this->is_connected() ) {
return new \WP_Error( '401', __( 'Connecting to the Library failed. Please try reloading the page and try again', 'elementor' ) );
}
$body_args = [
'id' => $id,
// Which API version is used.
'api_version' => ELEMENTOR_VERSION,
// Which language to return.
'site_lang' => get_bloginfo( 'language' ),
];
/**
* API: Template body args.
*
* Filters the body arguments send with the GET request when fetching the content.
*
* @since 1.0.0
*
* @param array $body_args Body arguments.
*/
$body_args = apply_filters( 'elementor/api/get_templates/body_args', $body_args );
$template_content = $this->request( 'get_template_content', $body_args, true );
return $template_content;
}
public function localize_settings( $settings ) {
$is_connected = $this->is_connected();
/** @var ConnectModule $connect */
$connect = Plugin::$instance->common->get_component( 'connect' );
return array_replace_recursive( $settings, [
'library_connect' => [
'is_connected' => $is_connected,
'subscription_plans' => $connect->get_subscription_plans( 'panel-library' ),
'base_access_level' => ConnectModule::ACCESS_LEVEL_CORE,
'current_access_level' => ConnectModule::ACCESS_LEVEL_CORE,
],
] );
}
public function library_connect_popup_seen() {
User::set_introduction_viewed( [
'introductionKey' => 'library_connect',
] );
}
/**
* @param \Elementor\Core\Common\Modules\Ajax\Module $ajax_manager
*/
public function register_ajax_actions( $ajax_manager ) {
$ajax_manager->register_ajax_action( 'library_connect_popup_seen', [ $this, 'library_connect_popup_seen' ] );
}
protected function get_app_info() {
return [
'user_common_data' => [
'label' => 'User Common Data',
'value' => get_user_option( $this->get_option_name(), get_current_user_id() ),
],
'connect_site_key' => [
'label' => 'Site Key',
'value' => get_option( 'elementor_connect_site_key' ),
],
'remote_info_library' => [
'label' => 'Remote Library Info',
'value' => get_option( 'elementor_remote_info_library' ),
],
];
}
protected function init() {
add_filter( 'elementor/editor/localize_settings', [ $this, 'localize_settings' ] );
add_action( 'elementor/ajax/register_actions', [ $this, 'register_ajax_actions' ] );
}
}

View File

@@ -0,0 +1,65 @@
export default class extends elementorModules.ViewModule {
addPopupPlugin() {
let counter = 0;
jQuery.fn.elementorConnect = function( options ) {
const settings = jQuery.extend( {
// These are the defaults.
success: () => location.reload(),
error: () => {
elementor.notifications.showToast( {
message: __( 'Unable to connect', 'elementor' ),
} );
},
}, options );
this.each( function() {
counter++;
const $this = jQuery( this ),
callbackId = 'cb' + ( counter ),
prevLibraryRoute = $e.routes.getHistory( 'library' ).reverse()[ 0 ].route,
tabName = prevLibraryRoute.split( '/' )[ 2 ],
UTMSource = `utm_source=editor-panel&utm_medium=wp-dash&utm_campaign=insert_${ tabName }`;
$this.attr( {
target: '_blank',
rel: 'opener',
href: $this.attr( 'href' ) + '&mode=popup&callback_id=' + callbackId + '&' + UTMSource,
} );
elementorCommon.elements.$window
.on( 'elementor/connect/success/' + callbackId, settings.success )
.on( 'elementor/connect/error/' + callbackId, settings.error );
} );
return this;
};
}
getDefaultSettings() {
return {
selectors: {
connectButton: '#elementor-template-library-connect__button',
},
};
}
getDefaultElements() {
return {
$connectButton: jQuery( this.getSettings( 'selectors.connectButton' ) ),
};
}
applyPopup() {
this.elements.$connectButton.elementorConnect();
}
onInit() {
super.onInit();
this.addPopupPlugin();
this.applyPopup();
}
}

View File

@@ -0,0 +1,201 @@
<?php
namespace Elementor\Core\Common\Modules\Connect;
use Elementor\Utils;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Common\Modules\Connect\Apps\Base_App;
use Elementor\Core\Common\Modules\Connect\Apps\Connect;
use Elementor\Core\Common\Modules\Connect\Apps\Library;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Module extends BaseModule {
const ACCESS_LEVEL_CORE = 0;
const ACCESS_LEVEL_PRO = 1;
const ACCESS_LEVEL_EXPERT = 20;
/**
* @since 2.3.0
* @access public
*/
public function get_name() {
return 'connect';
}
/**
* @var array
*/
protected $registered_apps = [];
/**
* Apps Instances.
*
* Holds the list of all the apps instances.
*
* @since 2.3.0
* @access protected
*
* @var Base_App[]
*/
protected $apps = [];
/**
* Registered apps categories.
*
* Holds the list of all the registered apps categories.
*
* @since 2.3.0
* @access protected
*
* @var array
*/
protected $categories = [];
protected $admin_page;
/**
* @since 2.3.0
* @access public
*/
public function __construct() {
$this->registered_apps = [
'connect' => Connect::get_class_name(),
'library' => Library::get_class_name(),
];
// Note: The priority 11 is for allowing plugins to add their register callback on elementor init.
add_action( 'elementor/init', [ $this, 'init' ], 11 );
}
/**
* Register default apps.
*
* Registers the default apps.
*
* @since 2.3.0
* @access public
*/
public function init() {
if ( is_admin() ) {
$this->admin_page = new Admin();
}
/**
* Register Elementor apps.
*
* Fires after Elementor registers the default apps.
*
* @since 2.3.0
*
* @param self $this The apps manager instance.
*/
do_action( 'elementor/connect/apps/register', $this );
foreach ( $this->registered_apps as $slug => $class ) {
$this->apps[ $slug ] = new $class();
}
}
/**
* @deprecated 3.1.0
*/
public function localize_settings() {
Plugin::$instance->modules_manager->get_modules( 'dev-tools' )->deprecation->deprecated_function( __METHOD__, '3.1.0' );
return [];
}
/**
* Register app.
*
* Registers an app.
*
* @since 2.3.0
* @access public
*
* @param string $slug App slug.
* @param string $class App full class name.
*
* @return self The updated apps manager instance.
*/
public function register_app( $slug, $class ) {
$this->registered_apps[ $slug ] = $class;
return $this;
}
/**
* Get app instance.
*
* Retrieve the app instance.
*
* @since 2.3.0
* @access public
*
* @param $slug
*
* @return Base_App|null
*/
public function get_app( $slug ) {
if ( isset( $this->apps[ $slug ] ) ) {
return $this->apps[ $slug ];
}
return null;
}
/**
* @since 2.3.0
* @access public
* @return Base_App[]
*/
public function get_apps() {
return $this->apps;
}
/**
* @since 2.3.0
* @access public
*/
public function register_category( $slug, $args ) {
$this->categories[ $slug ] = $args;
return $this;
}
/**
* @since 2.3.0
* @access public
*/
public function get_categories() {
return $this->categories;
}
/**
* @param $context
*
* @return array
*/
public function get_subscription_plans( $context ) {
return [
static::ACCESS_LEVEL_CORE => [
'label' => null,
'promotion_url' => null,
'color' => null,
],
static::ACCESS_LEVEL_PRO => [
'label' => 'Pro',
'promotion_url' => Utils::get_pro_link( "https://elementor.com/pro/?utm_source={$context}&utm_medium=wp-dash&utm_campaign=gopro" ),
'color' => '#92003B',
],
static::ACCESS_LEVEL_EXPERT => [
'label' => 'Expert',
'promotion_url' => Utils::get_pro_link( "https://elementor.com/pro/?utm_source={$context}&utm_medium=wp-dash&utm_campaign=goexpert" ),
'color' => '#010051',
],
];
}
}

View File

@@ -0,0 +1,3 @@
export { NavigateDown } from './navigate-down';
export { NavigateSelect } from './navigate-select';
export { NavigateUp } from './navigate-up';

View File

@@ -0,0 +1,9 @@
import CommandBase from 'elementor-api/modules/command-base';
export class NavigateDown extends CommandBase {
apply() {
this.component.getItemsView().activateNextItem();
}
}
export default NavigateDown;

View File

@@ -0,0 +1,9 @@
import CommandBase from 'elementor-api/modules/command-base';
export class NavigateSelect extends CommandBase {
apply( args ) {
this.component.getItemsView().goToActiveItem( args );
}
}
export default NavigateSelect;

View File

@@ -0,0 +1,9 @@
import CommandBase from 'elementor-api/modules/command-base';
export class NavigateUp extends CommandBase {
apply() {
this.component.getItemsView().activateNextItem( true );
}
}
export default NavigateUp;

View File

@@ -0,0 +1,85 @@
import ComponentModalBase from 'elementor-api/modules/component-modal-base';
import FinderLayout from './modal/views/layout';
import * as commands from './commands/';
export default class Component extends ComponentModalBase {
getNamespace() {
return 'finder';
}
defaultShortcuts() {
return {
'': {
keys: 'ctrl+e',
},
'navigate-down': {
keys: 'down',
scopes: [ this.getNamespace() ],
dependency: () => {
return this.getItemsView();
},
},
'navigate-up': {
keys: 'up',
scopes: [ this.getNamespace() ],
dependency: () => {
return this.getItemsView();
},
},
'navigate-select': {
keys: 'enter',
scopes: [ this.getNamespace() ],
dependency: () => {
return this.getItemsView().$activeItem;
},
},
};
}
defaultCommands() {
const modalCommands = super.defaultCommands();
return {
'navigate/down': () => {
elementorCommon.helpers.softDeprecated(
"$e.run( 'finder/navigate/down' )",
'3.0.0',
"$e.run( 'finder/navigate-down' )"
);
$e.run( 'finder/navigate-down' );
},
'navigate/up': () => {
elementorCommon.helpers.softDeprecated(
"$e.run( 'finder/navigate/up' )",
'3.0.0',
"$e.run( 'finder/navigate-up' )"
);
$e.run( 'finder/navigate-up' );
},
'navigate/select': ( event ) => {
elementorCommon.helpers.softDeprecated(
"$e.run( 'finder/navigate/select', event )",
'3.0.0',
"$e.run( 'finder/navigate-select', event )"
);
// TODO: Fix $e.shortcuts use args. ( args.event ).
$e.run( 'finder/navigate-select', event );
},
... modalCommands,
... this.importCommands( commands ),
};
}
getModalLayout() {
return FinderLayout;
}
getItemsView() {
return this.layout.modalContent.currentView.content.currentView;
}
}

View File

@@ -0,0 +1,15 @@
import Component from './component';
export default class extends elementorModules.Module {
onInit() {
// TODO: Temp fix, do not load finder in theme-builder.
// Better to pass into '$e' constructor the app owner. ( admin, editor, preview, iframe ).
if ( window.top !== window ) {
return;
}
this.channel = Backbone.Radio.channel( 'ELEMENTOR:finder' );
$e.components.register( new Component( { manager: this } ) );
}
}

View File

@@ -0,0 +1,11 @@
export default class extends Backbone.Model {
defaults() {
return {
description: '',
icon: 'settings',
url: '',
keywords: [],
actions: [],
};
}
}

View File

@@ -0,0 +1,96 @@
import Category from './category';
import DynamicCategory from './dynamic-category';
export default class extends Marionette.CompositeView {
id() {
return 'elementor-finder__results-container';
}
ui() {
this.selectors = {
noResults: '#elementor-finder__no-results',
categoryItem: '.elementor-finder__results__item',
};
return this.selectors;
}
events() {
return {
'mouseenter @ui.categoryItem': 'onCategoryItemMouseEnter',
};
}
getTemplate() {
return '#tmpl-elementor-finder-results-container';
}
getChildView( childModel ) {
return childModel.get( 'dynamic' ) ? DynamicCategory : Category;
}
initialize() {
this.$activeItem = null;
this.childViewContainer = '#elementor-finder__results';
this.collection = new Backbone.Collection( Object.values( elementorCommon.finder.getSettings( 'data' ) ) );
}
activateItem( $item ) {
if ( this.$activeItem ) {
this.$activeItem.removeClass( 'elementor-active' );
}
$item.addClass( 'elementor-active' );
this.$activeItem = $item;
}
activateNextItem( reverse ) {
const $allItems = jQuery( this.selectors.categoryItem );
let nextItemIndex = 0;
if ( this.$activeItem ) {
nextItemIndex = $allItems.index( this.$activeItem ) + ( reverse ? -1 : 1 );
if ( nextItemIndex >= $allItems.length ) {
nextItemIndex = 0;
} else if ( nextItemIndex < 0 ) {
nextItemIndex = $allItems.length - 1;
}
}
const $nextItem = $allItems.eq( nextItemIndex );
this.activateItem( $nextItem );
$nextItem[ 0 ].scrollIntoView( { block: 'nearest' } );
}
goToActiveItem( event ) {
const $a = this.$activeItem.children( 'a' ),
isControlClicked = $e.shortcuts.isControlEvent( event );
if ( isControlClicked ) {
$a.attr( 'target', '_blank' );
}
$a[ 0 ].click();
if ( isControlClicked ) {
$a.removeAttr( 'target' );
}
}
onCategoryItemMouseEnter( event ) {
this.activateItem( jQuery( event.currentTarget ) );
}
onChildviewToggleVisibility() {
const allCategoriesAreEmpty = this.children.every( ( child ) => ! child.isVisible );
this.ui.noResults.toggle( allCategoriesAreEmpty );
}
}

View File

@@ -0,0 +1,68 @@
import ItemView from './item';
import ItemModel from '../model/item';
export default class extends Marionette.CompositeView {
className() {
return 'elementor-finder__results__category';
}
getTemplate() {
return '#tmpl-elementor-finder__results__category';
}
getChildView() {
return ItemView;
}
initialize() {
this.childViewContainer = '.elementor-finder__results__category__items';
this.isVisible = true;
let items = this.model.get( 'items' );
if ( items ) {
items = Object.values( items );
}
this.collection = new Backbone.Collection( items, { model: ItemModel } );
}
filter( childModel ) {
const textFilter = this.getTextFilter();
if ( childModel.get( 'title' ).toLowerCase().indexOf( textFilter ) >= 0 ) {
return true;
}
return childModel.get( 'keywords' ).some( ( keyword ) => keyword.indexOf( textFilter ) >= 0 );
}
getTextFilter() {
return elementorCommon.finder.channel.request( 'filter:text' ).trim().toLowerCase();
}
toggleElement() {
const isCurrentlyVisible = ! ! this.children.length;
if ( isCurrentlyVisible !== this.isVisible ) {
this.isVisible = isCurrentlyVisible;
this.$el.toggle( isCurrentlyVisible );
this.triggerMethod( 'toggle:visibility' );
}
}
onRender() {
this.listenTo( elementorCommon.finder.channel, 'filter:change', this.onFilterChange.bind( this ) );
}
onFilterChange() {
this._renderChildren();
}
onRenderCollection() {
this.toggleElement();
}
}

View File

@@ -0,0 +1,49 @@
import CategoriesView from './categories';
export default class extends Marionette.LayoutView {
id() {
return 'elementor-finder';
}
getTemplate() {
return '#tmpl-elementor-finder';
}
ui() {
return {
searchInput: '#elementor-finder__search__input',
};
}
events() {
return {
'input @ui.searchInput': 'onSearchInputInput',
};
}
regions() {
return {
content: '#elementor-finder__content',
};
}
showCategoriesView() {
this.content.show( new CategoriesView() );
}
onSearchInputInput() {
const value = this.ui.searchInput.val();
if ( value ) {
elementorCommon.finder.channel
.reply( 'filter:text', value )
.trigger( 'filter:change' );
if ( ! ( this.content.currentView instanceof CategoriesView ) ) {
this.showCategoriesView();
}
}
this.content.currentView.$el.toggle( ! ! value );
}
}

View File

@@ -0,0 +1,53 @@
import Category from './category';
export default class extends Category {
className() {
return super.className() + ' elementor-finder__results__category--dynamic';
}
ui() {
return {
title: '.elementor-finder__results__category__title',
};
}
fetchData() {
this.ui.loadingIcon.show();
elementorCommon.ajax.addRequest( 'finder_get_category_items', {
data: {
category: this.model.get( 'name' ),
filter: this.getTextFilter(),
},
success: ( data ) => {
if ( this.isDestroyed ) {
return;
}
this.collection.set( data );
this.toggleElement();
this.ui.loadingIcon.hide();
},
} );
}
filter() {
return true;
}
onFilterChange() {
this.fetchData();
}
onRender() {
super.onRender();
this.ui.loadingIcon = jQuery( '<i>', { class: 'eicon-loading eicon-animation-spin' } );
this.ui.title.after( this.ui.loadingIcon );
this.fetchData();
}
}

View File

@@ -0,0 +1,9 @@
export default class extends Marionette.ItemView {
className() {
return 'elementor-finder__results__item';
}
getTemplate() {
return '#tmpl-elementor-finder__results__item';
}
}

View File

@@ -0,0 +1,41 @@
import ModalContent from './content';
export default class extends elementorModules.common.views.modal.Layout {
getModalOptions() {
return {
id: 'elementor-finder__modal',
draggable: true,
effects: {
show: 'show',
hide: 'hide',
},
position: {
enable: false,
},
};
}
getLogoOptions() {
return {
title: __( 'Finder', 'elementor' ),
};
}
initialize( ...args ) {
super.initialize( ...args );
this.showLogo();
this.showContentView();
}
showContentView() {
this.modalContent.show( new ModalContent() );
}
showModal( ...args ) {
super.showModal( ...args );
this.modalContent.currentView.ui.searchInput.focus();
}
}

View File

@@ -0,0 +1,166 @@
#elementor-finder {
&__modal {
background: none;
z-index: 99999; // Above Theme Builder Modal
.dialog-widget-content {
$modal-width: 650px;
width: $modal-width;
max-width: 98%;
top: 18vh;
left: calc( 50% - #{$modal-width} / 2 );
}
.dialog-message {
height: initial;
min-height: 0;
padding: 0;
text-align: $start;
}
}
&__search {
padding: 14px 15px 14px 21px;
display: flex;
box-shadow: 0 -3px 15px 6px rgba(0, 0, 0, 0.03);
i {
font-size: 16px;
color: $editor-light;
font-weight: bold;
@include margin-end(15px);
}
&__input {
border: none;
background: none;
outline: none;
padding: 0;
margin: 0;
flex-grow: 1;
font-size: 14px;
color: $editor-darkest;
box-shadow: none;
&::placeholder {
color: $editor-light;
font-style: italic;
font-weight: 300;
}
}
}
&__results {
max-height: 50vh;
overflow: auto;
}
&__no-results {
display: none;
padding: 20px;
color: $editor-light;
}
}
.elementor-finder__results {
&__category {
position: relative;
&__title {
padding: 5px 25px;
color: $editor-dark;
background-color: $editor-background;
font-size: 9px;
text-transform: uppercase;
}
&--dynamic {
.elementor-finder__results__category__items {
min-height: 26px;
}
}
.eicon-loading {
display: none;
position: absolute;
@include end(10px);
top: 30px;
color: $editor-info;
font-size: 14px;
}
}
&__item {
$item-height: 35px;
display: flex;
a {
text-decoration: none;
box-shadow: none;
outline: none;
}
&.elementor-active {
background-color: $editor-info;
box-shadow: none;
outline: none;
* {
color: #fff;
}
}
&:not(.elementor-active) {
.elementor-finder__results__item__actions {
display: none;
}
}
&__link {
display: flex;
align-items: center;
height: $item-height;
flex-grow: 1;
}
&__icon {
width: 60px;
text-align: center;
color: $editor-dark;
font-size: 17px;
}
&__title {
color: $editor-dark;
font-size: 13px;
}
&__description {
@include margin-start(5px);
color: $editor-light;
font-style: italic;
}
&__actions {
display: flex;
}
&__action {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: $item-height;
&:hover {
background-color: rgba(0, 0, 0, .07);
}
}
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Elementor\Core\Common\Modules\Finder;
use Elementor\Core\Base\Base_Object;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Base Category
*
* Base class for Elementor Finder categories.
*/
abstract class Base_Category extends Base_Object {
/**
* Get title.
*
* @since 2.3.0
* @abstract
* @access public
*
* @return string
*/
abstract public function get_title();
/**
* Get category items.
*
* @since 2.3.0
* @abstract
* @access public
*
* @param array $options
*
* @return array
*/
abstract public function get_category_items( array $options = [] );
/**
* Is dynamic.
*
* Determine if the category is dynamic.
*
* @since 2.3.0
* @access public
*
* @return bool
*/
public function is_dynamic() {
return false;
}
/**
* Get init settings.
*
* @since 2.3.0
* @access protected
*
* @return array
*/
protected function get_init_settings() {
$settings = [
'title' => $this->get_title(),
'dynamic' => $this->is_dynamic(),
];
if ( ! $settings['dynamic'] ) {
$settings['items'] = $this->get_category_items();
}
return $settings;
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Elementor\Core\Common\Modules\Finder;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class Categories_Manager {
/**
* @access private
*
* @var Base_Category[]
*/
private $categories;
/**
* @var array
*/
private $categories_list = [
'edit',
'general',
'create',
'site',
'settings',
'tools',
];
/**
* Add category.
*
* @since 2.3.0
* @access public
* @param string $category_name
* @param Base_Category $category
*/
public function add_category( $category_name, Base_Category $category ) {
$this->categories[ $category_name ] = $category;
}
/**
* Get categories.
*
* @since 2.3.0
* @access public
* @param string $category
*
* @return Base_Category|Base_Category[]|null
*/
public function get_categories( $category = '' ) {
if ( ! $this->categories ) {
$this->init_categories();
}
if ( $category ) {
if ( isset( $this->categories[ $category ] ) ) {
return $this->categories[ $category ];
}
return null;
}
return $this->categories;
}
/**
* Init categories.
*
* Used to initialize finder default categories.
* @since 2.3.0
* @access private
*/
private function init_categories() {
foreach ( $this->categories_list as $category_name ) {
$class_name = __NAMESPACE__ . '\Categories\\' . $category_name;
$this->add_category( $category_name, new $class_name() );
}
/**
* Elementor Finder categories init.
*
* Fires after Elementor Finder initialize it's native categories.
*
* This hook should be used to add your own Finder categories.
*
* @since 2.3.0
*
* @param Categories_Manager $this.
*/
do_action( 'elementor/finder/categories/init', $this );
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Elementor\Core\Common\Modules\Finder\Categories;
use Elementor\Core\Common\Modules\Finder\Base_Category;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Create Category
*
* Provides items related to creation of new posts/pages/templates etc.
*/
class Create extends Base_Category {
/**
* Get title.
*
* @since 2.3.0
* @access public
*
* @return string
*/
public function get_title() {
return __( 'Create', 'elementor' );
}
/**
* Get category items.
*
* @since 2.3.0
* @access public
*
* @param array $options
*
* @return array
*/
public function get_category_items( array $options = [] ) {
$elementor_supported_post_types = get_post_types_by_support( 'elementor' );
$items = [];
foreach ( $elementor_supported_post_types as $post_type ) {
$post_type_object = get_post_type_object( $post_type );
// If there is an old post type from inactive plugins
if ( ! $post_type_object ) {
continue;
}
if ( Source_Local::CPT === $post_type ) {
$url = admin_url( Source_Local::ADMIN_MENU_SLUG . '#add_new' );
} else {
$url = Utils::get_create_new_post_url( $post_type );
}
$items[ $post_type ] = [
/* translators: %s the title of the post type */
'title' => sprintf( __( 'Add New %s', 'elementor' ), $post_type_object->labels->singular_name ),
'icon' => 'plus-circle-o',
'url' => $url,
'keywords' => [ 'post', 'page', 'template', 'new', 'create' ],
];
}
return $items;
}
}

View File

@@ -0,0 +1,135 @@
<?php
namespace Elementor\Core\Common\Modules\Finder\Categories;
use Elementor\Core\Base\Document;
use Elementor\Core\Common\Modules\Finder\Base_Category;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Edit Category
*
* Provides items related to editing of posts/pages/templates etc.
*/
class Edit extends Base_Category {
/**
* Get title.
*
* @since 2.3.0
* @access public
*
* @return string
*/
public function get_title() {
return __( 'Edit', 'elementor' );
}
/**
* Is dynamic.
*
* Determine if the category is dynamic.
*
* @since 2.3.0
* @access public
*
* @return bool
*/
public function is_dynamic() {
return true;
}
/**
* Get category items.
*
* @since 2.3.0
* @access public
*
* @param array $options
*
* @return array
*/
public function get_category_items( array $options = [] ) {
$post_types = get_post_types( [
'exclude_from_search' => false,
] );
$post_types[] = Source_Local::CPT;
$document_types = Plugin::$instance->documents->get_document_types( [
'is_editable' => true,
'show_in_finder' => true,
] );
$recently_edited_query_args = [
'post_type' => $post_types,
'post_status' => [ 'publish', 'draft', 'private', 'pending', 'future' ],
'posts_per_page' => '10',
'meta_query' => [
[
'key' => '_elementor_edit_mode',
'value' => 'builder',
],
[
'relation' => 'or',
[
'key' => Document::TYPE_META_KEY,
'compare' => 'NOT EXISTS',
],
[
'key' => Document::TYPE_META_KEY,
'value' => array_keys( $document_types ),
],
],
],
'orderby' => 'modified',
's' => $options['filter'],
];
$recently_edited_query = new \WP_Query( $recently_edited_query_args );
$items = [];
/** @var \WP_Post $post */
foreach ( $recently_edited_query->posts as $post ) {
$document = Plugin::$instance->documents->get( $post->ID );
if ( ! $document ) {
continue;
}
$is_template = Source_Local::CPT === $post->post_type;
$description = $document->get_title();
$icon = 'document-file';
if ( $is_template ) {
$description = __( 'Template', 'elementor' ) . ' / ' . $description;
$icon = 'post-title';
}
$items[] = [
'icon' => $icon,
'title' => esc_html( $post->post_title ),
'description' => $description,
'url' => $document->get_edit_url(),
'actions' => [
[
'name' => 'view',
'url' => $document->get_permalink(),
'icon' => 'preview-medium',
],
],
];
}
return $items;
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Elementor\Core\Common\Modules\Finder\Categories;
use Elementor\Core\Common\Modules\Finder\Base_Category;
use Elementor\Core\RoleManager\Role_Manager;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* General Category
*
* Provides general items related to Elementor Admin.
*/
class General extends Base_Category {
/**
* Get title.
*
* @since 2.3.0
* @access public
*
* @return string
*/
public function get_title() {
return __( 'General', 'elementor' );
}
/**
* Get category items.
*
* @since 2.3.0
* @access public
*
* @param array $options
*
* @return array
*/
public function get_category_items( array $options = [] ) {
return [
'saved-templates' => [
'title' => _x( 'Saved Templates', 'Template Library', 'elementor' ),
'icon' => 'library-save',
'url' => Source_Local::get_admin_url(),
'keywords' => [ 'template', 'section', 'page', 'library' ],
],
'system-info' => [
'title' => __( 'System Info', 'elementor' ),
'icon' => 'info-circle-o',
'url' => admin_url( 'admin.php?page=elementor-system-info' ),
'keywords' => [ 'system', 'info', 'environment', 'elementor' ],
],
'role-manager' => [
'title' => __( 'Role Manager', 'elementor' ),
'icon' => 'person',
'url' => Role_Manager::get_url(),
'keywords' => [ 'role', 'manager', 'user', 'elementor' ],
],
'knowledge-base' => [
'title' => __( 'Knowledge Base', 'elementor' ),
'url' => admin_url( 'admin.php?page=go_knowledge_base_site' ),
'keywords' => [ 'help', 'knowledge', 'docs', 'elementor' ],
],
'theme-builder' => [
'title' => __( 'Theme Builder', 'elementor' ),
'icon' => 'library-save',
'url' => Plugin::$instance->app->get_settings( 'menu_url' ),
'keywords' => [ 'template', 'header', 'footer', 'single', 'archive', 'search', '404', 'library' ],
],
];
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Elementor\Core\Common\Modules\Finder\Categories;
use Elementor\Core\Common\Modules\Finder\Base_Category;
use Elementor\Settings as ElementorSettings;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Settings Category
*
* Provides items related to Elementor's settings.
*/
class Settings extends Base_Category {
/**
* Get title.
*
* @since 2.3.0
* @access public
*
* @return string
*/
public function get_title() {
return __( 'Settings', 'elementor' );
}
/**
* Get category items.
*
* @since 2.3.0
* @access public
*
* @param array $options
*
* @return array
*/
public function get_category_items( array $options = [] ) {
$settings_url = ElementorSettings::get_url();
return [
'general-settings' => [
'title' => __( 'General Settings', 'elementor' ),
'url' => $settings_url,
'keywords' => [ 'general', 'settings', 'elementor' ],
],
'advanced' => [
'title' => __( 'Advanced', 'elementor' ),
'url' => $settings_url . '#tab-advanced',
'keywords' => [ 'advanced', 'settings', 'elementor' ],
],
'experiments' => [
'title' => __( 'Experiments', 'elementor' ),
'url' => $settings_url . '#tab-experiments',
'keywords' => [ 'settings', 'elementor', 'experiments' ],
],
];
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace Elementor\Core\Common\Modules\Finder\Categories;
use Elementor\Core\Common\Modules\Finder\Base_Category;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Site Category
*
* Provides general site items.
*/
class Site extends Base_Category {
/**
* Get title.
*
* @since 2.3.0
* @access public
*
* @return string
*/
public function get_title() {
return __( 'Site', 'elementor' );
}
/**
* Get category items.
*
* @since 2.3.0
* @access public
*
* @param array $options
*
* @return array
*/
public function get_category_items( array $options = [] ) {
return [
'homepage' => [
'title' => __( 'Homepage', 'elementor' ),
'url' => home_url(),
'icon' => 'home-heart',
'keywords' => [ 'home', 'page' ],
],
'wordpress-dashboard' => [
'title' => __( 'Dashboard', 'elementor' ),
'icon' => 'dashboard',
'url' => admin_url(),
'keywords' => [ 'dashboard', 'wordpress' ],
],
'wordpress-menus' => [
'title' => __( 'Menus', 'elementor' ),
'icon' => 'wordpress',
'url' => admin_url( 'nav-menus.php' ),
'keywords' => [ 'menu', 'wordpress' ],
],
'wordpress-themes' => [
'title' => __( 'Themes', 'elementor' ),
'icon' => 'wordpress',
'url' => admin_url( 'themes.php' ),
'keywords' => [ 'themes', 'wordpress' ],
],
'wordpress-customizer' => [
'title' => __( 'Customizer', 'elementor' ),
'icon' => 'wordpress',
'url' => admin_url( 'customize.php' ),
'keywords' => [ 'customizer', 'wordpress' ],
],
'wordpress-plugins' => [
'title' => __( 'Plugins', 'elementor' ),
'icon' => 'wordpress',
'url' => admin_url( 'plugins.php' ),
'keywords' => [ 'plugins', 'wordpress' ],
],
'wordpress-users' => [
'title' => __( 'Users', 'elementor' ),
'icon' => 'wordpress',
'url' => admin_url( 'users.php' ),
'keywords' => [ 'users', 'profile', 'wordpress' ],
],
];
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Elementor\Core\Common\Modules\Finder\Categories;
use Elementor\Core\Common\Modules\Finder\Base_Category;
use Elementor\Tools as ElementorTools;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Tools Category
*
* Provides items related to Elementor's tools.
*/
class Tools extends Base_Category {
/**
* Get title.
*
* @since 2.3.0
* @access public
*
* @return string
*/
public function get_title() {
return __( 'Tools', 'elementor' );
}
/**
* Get category items.
*
* @since 2.3.0
* @access public
*
* @param array $options
*
* @return array
*/
public function get_category_items( array $options = [] ) {
$tools_url = ElementorTools::get_url();
return [
'tools' => [
'title' => __( 'Tools', 'elementor' ),
'icon' => 'tools',
'url' => $tools_url,
'keywords' => [ 'tools', 'regenerate css', 'safe mode', 'debug bar', 'sync library', 'elementor' ],
],
'replace-url' => [
'title' => __( 'Replace URL', 'elementor' ),
'icon' => 'tools',
'url' => $tools_url . '#tab-replace_url',
'keywords' => [ 'tools', 'replace url', 'domain', 'elementor' ],
],
'version-control' => [
'title' => __( 'Version Control', 'elementor' ),
'icon' => 'time-line',
'url' => $tools_url . '#tab-versions',
'keywords' => [ 'tools', 'version', 'control', 'rollback', 'beta', 'elementor' ],
],
'maintenance-mode' => [
'title' => __( 'Maintenance Mode', 'elementor' ),
'icon' => 'tools',
'url' => $tools_url . '#tab-maintenance_mode',
'keywords' => [ 'tools', 'maintenance', 'coming soon', 'elementor' ],
],
];
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace Elementor\Core\Common\Modules\Finder;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Common\Modules\Ajax\Module as Ajax;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Finder Module
*
* Responsible for initializing Elementor Finder functionality
*/
class Module extends BaseModule {
/**
* Categories manager.
*
* @access private
*
* @var Categories_Manager
*/
private $categories_manager;
/**
* Module constructor.
*
* @since 2.3.0
* @access public
*/
public function __construct() {
$this->categories_manager = new Categories_Manager();
$this->add_template();
add_action( 'elementor/ajax/register_actions', [ $this, 'register_ajax_actions' ] );
}
/**
* Get name.
*
* @since 2.3.0
* @access public
*
* @return string
*/
public function get_name() {
return 'finder';
}
/**
* Add template.
*
* @since 2.3.0
* @access public
*/
public function add_template() {
Plugin::$instance->common->add_template( __DIR__ . '/template.php' );
}
/**
* Register ajax actions.
*
* @since 2.3.0
* @access public
*
* @param Ajax $ajax
*/
public function register_ajax_actions( Ajax $ajax ) {
$ajax->register_ajax_action( 'finder_get_category_items', [ $this, 'ajax_get_category_items' ] );
}
/**
* Ajax get category items.
*
* @since 2.3.0
* @access public
*
* @param array $data
*
* @return array
*/
public function ajax_get_category_items( array $data ) {
$category = $this->categories_manager->get_categories( $data['category'] );
return $category->get_category_items( $data );
}
/**
* Get init settings.
*
* @since 2.3.0
* @access protected
*
* @return array
*/
protected function get_init_settings() {
$categories = $this->categories_manager->get_categories();
$categories_data = [];
foreach ( $categories as $category_name => $category ) {
$categories_data[ $category_name ] = array_merge( $category->get_settings(), [ 'name' => $category_name ] );
}
$categories_data = apply_filters( 'elementor/finder/categories', $categories_data );
return [
'data' => $categories_data,
];
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Elementor\Modules\Finder;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
?>
<script type="text/template" id="tmpl-elementor-finder">
<div id="elementor-finder__search">
<i class="eicon-search"></i>
<input id="elementor-finder__search__input" placeholder="<?php echo __( 'Type to find anything in Elementor', 'elementor' ); ?>">
</div>
<div id="elementor-finder__content"></div>
</script>
<script type="text/template" id="tmpl-elementor-finder-results-container">
<div id="elementor-finder__no-results"><?php echo __( 'No Results Found', 'elementor' ); ?></div>
<div id="elementor-finder__results"></div>
</script>
<script type="text/template" id="tmpl-elementor-finder__results__category">
<div class="elementor-finder__results__category__title">{{{ title }}}</div>
<div class="elementor-finder__results__category__items"></div>
</script>
<script type="text/template" id="tmpl-elementor-finder__results__item">
<a href="{{ url }}" class="elementor-finder__results__item__link">
<div class="elementor-finder__results__item__icon">
<i class="eicon-{{{ icon }}}"></i>
</div>
<div class="elementor-finder__results__item__title">{{{ title }}}</div>
<# if ( description ) { #>
<div class="elementor-finder__results__item__description">- {{{ description }}}</div>
<# } #>
</a>
<# if ( actions.length ) { #>
<div class="elementor-finder__results__item__actions">
<# jQuery.each( actions, function() { #>
<a class="elementor-finder__results__item__action elementor-finder__results__item__action--{{ this.name }}" href="{{ this.url }}" target="_blank">
<i class="eicon-{{{ this.icon }}}"></i>
</a>
<# } ); #>
</div>
<# } #>
</script>