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,35 @@
import ComponentBase from 'elementor-api/modules/component-base';
export default class Component extends ComponentBase {
getNamespace() {
return 'panel/history';
}
defaultTabs() {
return {
actions: { title: __( 'Actions', 'elementor' ) },
revisions: { title: __( 'Revisions', 'elementor' ) },
};
}
defaultShortcuts() {
return {
actions: {
keys: 'ctrl+shift+h',
},
};
}
renderTab( tab ) {
elementor.getPanelView().setPage( 'historyPage' ).showView( tab );
}
activate() {
// Activate the tab component itself.
$e.components.activate( this.getTabRoute( this.currentTab ) );
}
getTabsWrapperSelector() {
return '#elementor-panel-elements-navigation';
}
}

View File

@@ -0,0 +1,27 @@
import ComponentBase from 'elementor-api/modules/component-base';
import * as commands from 'elementor-document/history/commands/';
export default class Component extends ComponentBase {
getNamespace() {
return 'panel/history/actions';
}
defaultCommands() {
return this.importCommands( commands );
}
defaultShortcuts() {
return {
undo: {
keys: 'ctrl+z',
exclude: [ 'input' ],
scopes: [ 'panel', 'navigator' ],
},
redo: {
keys: 'ctrl+shift+z, ctrl+y',
exclude: [ 'input' ],
scopes: [ 'panel', 'navigator' ],
},
};
}
}

View File

@@ -0,0 +1,13 @@
export default class extends Marionette.ItemView {
getTemplate() {
return '#tmpl-elementor-panel-history-no-items';
}
id() {
return 'elementor-panel-history-no-items';
}
onDestroy() {
this._parent.$el.removeClass( 'elementor-empty' );
}
}

View File

@@ -0,0 +1,15 @@
module.exports = Backbone.Model.extend( {
defaults: {
id: 0,
type: '',
status: 'not_applied',
title: '',
subTitle: '',
action: '',
history: {},
},
initialize: function() {
this.set( 'items', new Backbone.Collection() );
},
} );

View File

@@ -0,0 +1,15 @@
export default class extends Marionette.ItemView {
getTemplate() {
return '#tmpl-elementor-panel-history-item';
}
className() {
return 'elementor-history-item elementor-history-item-' + this.model.get( 'status' );
}
triggers() {
return {
click: 'click',
};
}
}

View File

@@ -0,0 +1,259 @@
import ItemModel from './item-model';
/**
* TODO: consider refactor this class.
* TODO: should be `Document/History` component.
* TODO: should be attached to elementor.history.history + BC.
*/
export default class HistoryManager {
currentItemID = null;
items = new Backbone.Collection( [], { model: ItemModel } );
active = true;
translations = {
add: __( 'Added', 'elementor' ),
change: __( 'Edited', 'elementor' ),
disable: __( 'Disabled', 'elementor' ),
duplicate: __( 'Duplicate', 'elementor' ),
enable: __( 'Enabled', 'elementor' ),
move: __( 'Moved', 'elementor' ),
paste: __( 'Pasted', 'elementor' ),
paste_style: __( 'Style Pasted', 'elementor' ),
remove: __( 'Removed', 'elementor' ),
reset_style: __( 'Style Reset', 'elementor' ),
reset_settings: __( 'Settings Reset', 'elementor' ),
};
constructor( document ) {
this.document = document;
this.currentItem = new Backbone.Model( {
id: 0,
} );
}
getActionLabel( itemData ) {
// TODO: this function should be static.
if ( this.translations[ itemData.type ] ) {
return this.translations[ itemData.type ];
}
return itemData.type;
}
navigate( isRedo ) {
const currentItem = this.items.find( ( model ) => {
return 'not_applied' === model.get( 'status' );
} ),
currentItemIndex = this.items.indexOf( currentItem ),
requiredIndex = isRedo ? currentItemIndex - 1 : currentItemIndex + 1;
if ( ( ! isRedo && ! currentItem ) || requiredIndex < 0 || requiredIndex >= this.items.length ) {
return;
}
this.doItem( requiredIndex );
}
setActive( value ) {
this.active = value;
}
getActive( value ) {
return this.active;
}
getItems() {
return this.items;
}
startItem( itemData ) {
this.currentItemID = this.addItem( itemData );
return this.currentItemID;
}
endItem( id ) {
if ( this.currentItemID !== id ) {
return;
}
this.currentItemID = null;
}
deleteItem( id ) {
const item = this.items.findWhere( {
id: id,
} );
this.items.remove( item );
this.currentItemID = null;
}
isItemStarted() {
return null !== this.currentItemID;
}
getCurrentId() {
return this.currentItemID;
}
addItem( itemData ) {
if ( ! this.getActive() ) {
return;
}
if ( ! this.items.length ) {
this.items.add( {
status: 'not_applied',
title: __( 'Editing Started', 'elementor' ),
subTitle: '',
action: '',
editing_started: true,
} );
}
// Remove old applied items from top of list
while ( this.items.length && 'applied' === this.items.first().get( 'status' ) ) {
this.items.shift();
}
const id = this.currentItemID ? this.currentItemID : new Date().getTime();
let currentItem = this.items.findWhere( {
id: id,
} );
if ( ! currentItem ) {
currentItem = new ItemModel( {
id: id,
title: itemData.title,
subTitle: itemData.subTitle,
action: this.getActionLabel( itemData ),
type: itemData.type,
} );
this.startItemTitle = '';
this.startItemAction = '';
}
currentItem.get( 'items' ).add( itemData, { at: 0 } );
this.items.add( currentItem, { at: 0 } );
this.updateCurrentItem( currentItem );
return id;
}
doItem( index ) {
// Don't track while restoring the item
this.setActive( false );
const item = this.items.at( index );
if ( 'not_applied' === item.get( 'status' ) ) {
this.undoItem( index );
} else {
this.redoItem( index );
}
this.setActive( true );
const panel = elementor.getPanelView(),
panelPage = panel.getCurrentPageView(),
editedElementView = panelPage.getOption( 'editedElementView' );
let viewToScroll;
if ( $e.routes.isPartOf( 'panel/editor' ) && editedElementView ) {
if ( editedElementView.isDestroyed ) {
// If the the element isn't exist - show the history panel
$e.route( 'panel/history/actions' );
} else {
// If element exist - render again, maybe the settings has been changed
viewToScroll = editedElementView;
}
} else if ( item instanceof Backbone.Model && item.get( 'items' ).length ) {
const historyItem = item.get( 'items' ).first();
if ( historyItem.get( 'restore' ) ) {
let container = 'sub-add' === historyItem.get( 'type' ) ?
historyItem.get( 'data' ).containerToRestore :
historyItem.get( 'container' ) || historyItem.get( 'containers' );
if ( Array.isArray( container ) ) {
container = container[ 0 ];
}
if ( container ) {
viewToScroll = container.lookup().view;
}
}
}
$e.internal( 'document/save/set-is-modified', {
status: item.get( 'id' ) !== this.document.editor.lastSaveHistoryId,
} );
this.updateCurrentItem( item );
if ( viewToScroll && ! elementor.helpers.isInViewport( viewToScroll.$el[ 0 ], elementor.$previewContents.find( 'html' )[ 0 ] ) ) {
elementor.helpers.scrollToView( viewToScroll.$el );
}
}
undoItem( index ) {
for ( let stepNum = 0; stepNum < index; stepNum++ ) {
const item = this.items.at( stepNum );
if ( 'not_applied' === item.get( 'status' ) ) {
item.get( 'items' ).each( function( subItem ) {
const restore = subItem.get( 'restore' );
if ( restore ) {
restore( subItem );
}
} );
item.set( 'status', 'applied' );
}
}
}
redoItem( index ) {
for ( let stepNum = this.items.length - 1; stepNum >= index; stepNum-- ) {
const item = this.items.at( stepNum );
if ( 'applied' === item.get( 'status' ) ) {
var reversedSubItems = _.toArray( item.get( 'items' ).models ).reverse();
_( reversedSubItems ).each( function( subItem ) {
const restore = subItem.get( 'restore' );
if ( restore ) {
restore( subItem, true );
}
} );
item.set( 'status', 'not_applied' );
}
}
}
updateCurrentItem( item ) {
// Save last selected item.
this.currentItem = item;
this.updatePanelPageCurrentItem();
}
updatePanelPageCurrentItem() {
if ( $e.routes.is( 'panel/history/actions' ) ) {
elementor.getPanelView().getCurrentPageView().getCurrentTab().updateCurrentItem();
}
}
}

View File

@@ -0,0 +1,63 @@
import ItemView from './item-view';
import EmptyView from './empty';
module.exports = Marionette.CompositeView.extend( {
id: 'elementor-panel-history',
template: '#tmpl-elementor-panel-history-tab',
childView: ItemView,
childViewContainer: '#elementor-history-list',
emptyView: EmptyView,
currentItem: null,
updateCurrentItem: function() {
if ( this.children.length <= 1 ) {
return;
}
_.defer( () => {
// Set current item - the first not applied item
const currentItem = this.collection.find( function( model ) {
return 'not_applied' === model.get( 'status' );
} ),
currentView = this.children.findByModel( currentItem );
if ( ! currentView ) {
return;
}
const currentItemClass = 'elementor-history-item-current';
if ( this.currentItem ) {
this.currentItem.removeClass( currentItemClass );
}
this.currentItem = currentView.$el;
this.currentItem.addClass( currentItemClass );
} );
},
onRender: function() {
this.updateCurrentItem();
},
onRenderEmpty: function() {
this.$el.addClass( 'elementor-empty' );
},
onChildviewClick: function( childView, event ) {
if ( childView.$el === this.currentItem ) {
return;
}
const collection = event.model.collection,
index = collection.findIndex( event.model );
$e.run( 'panel/history/actions/do', { index } );
},
} );

View File

@@ -0,0 +1,33 @@
import Component from './component';
import HistoryComponent from './history/component';
import RevisionsComponent from './revisions/component';
import PanelPage from './panel-page';
export default class Manager {
constructor() {
elementorCommon.elements.$window.on( 'elementor:loaded', this.init );
}
init() {
$e.components.register( new Component() );
$e.components.register( new HistoryComponent() );
$e.components.register( new RevisionsComponent() );
elementor.on( 'panel:init', () => {
elementor.getPanelView().addPage( 'historyPage', {
view: PanelPage,
title: __( 'History', 'elementor' ),
} );
} );
}
get history() {
elementorCommon.helpers.softDeprecated( 'elementor.history.history', '2.9.0', 'elementor.documents.getCurrent().history' );
return elementor.documents.getCurrent().history;
}
get revisions() {
elementorCommon.helpers.softDeprecated( 'elementor.history.revisions', '2.9.0', 'elementor.documents.getCurrent().revisions' );
return elementor.documents.getCurrent().revisions;
}
}

View File

@@ -0,0 +1,84 @@
var TabHistoryView = require( './history/panel-tab' );
import TabRevisionsLoadingView from './revisions/panel/loading';
import TabRevisionsView from './revisions/panel/tab';
import TabRevisionsEmptyView from './revisions/panel/empty';
module.exports = Marionette.LayoutView.extend( {
template: '#tmpl-elementor-panel-history-page',
regions: {
content: '#elementor-panel-history-content',
},
ui: {
tabs: '.elementor-panel-navigation-tab',
},
regionViews: {},
currentTab: null,
/**
* @type {Document}
*/
document: null,
initialize: function( options ) {
this.document = options.document || elementor.documents.getCurrent();
this.initRegionViews();
},
initRegionViews: function() {
const historyItems = this.document.history.getItems();
this.regionViews = {
actions: {
view: () => {
return TabHistoryView;
},
options: {
collection: historyItems,
history: this.document.history,
},
},
revisions: {
view: () => {
const revisionsItems = this.document.revisions.getItems();
if ( ! revisionsItems ) {
return TabRevisionsLoadingView;
}
if ( 1 === revisionsItems.length && 'current' === revisionsItems.models[ 0 ].get( 'type' ) ) {
return TabRevisionsEmptyView;
}
return TabRevisionsView;
},
options: {
document: this.document,
},
},
};
},
getCurrentTab: function() {
return this.currentTab;
},
showView: function( viewName ) {
const viewDetails = this.regionViews[ viewName ],
options = viewDetails.options || {},
View = viewDetails.view();
if ( this.currentTab && this.currentTab.constructor === View ) {
return;
}
this.currentTab = new View( options );
this.content.show( this.currentTab );
},
} );

View File

@@ -0,0 +1,8 @@
var RevisionModel = require( './model' );
module.exports = Backbone.Collection.extend( {
model: RevisionModel,
comparator: function( model ) {
return -model.get( 'timestamp' );
},
} );

View File

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

View File

@@ -0,0 +1,2 @@
export { Down } from './down';
export { Up } from './up';

View File

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

View File

@@ -0,0 +1,36 @@
import ComponentBase from 'elementor-api/modules/component-base';
import * as commands from './commands/';
import * as hooks from './hooks/';
export default class Component extends ComponentBase {
getNamespace() {
return 'panel/history/revisions';
}
defaultCommands() {
return this.importCommands( commands );
}
defaultHooks() {
return this.importHooks( hooks );
}
defaultShortcuts() {
return {
up: {
keys: 'up',
scopes: [ this.getNamespace() ],
},
down: {
keys: 'down',
scopes: [ this.getNamespace() ],
},
};
}
navigate( up ) {
if ( elementor.documents.getCurrent().revisions.getItems().length > 1 ) {
elementor.getPanelView().getCurrentPageView().currentTab.navigate( up );
}
}
}

View File

@@ -0,0 +1,32 @@
import HookDataAfter from 'elementor-api/modules/hooks/data/after';
export class RevisionsAfterSave extends HookDataAfter {
getCommand() {
return 'document/save/save';
}
getId() {
return 'revisions-after-save';
}
apply( args, result ) {
const { data } = result,
revisionsModule = elementor.documents.getCurrent().revisions;
if ( data.latest_revisions ) {
revisionsModule.addRevisions( data.latest_revisions );
}
revisionsModule.requestRevisions( () => {
if ( data.revisions_ids ) {
const revisionsToKeep = revisionsModule.revisions.filter( ( revision ) => {
return -1 !== data.revisions_ids.indexOf( revision.get( 'id' ) );
} );
revisionsModule.revisions.reset( revisionsToKeep );
}
} );
}
}
export default RevisionsAfterSave;

View File

@@ -0,0 +1 @@
export { RevisionsAfterSave } from './data/save';

View File

@@ -0,0 +1,97 @@
const RevisionsCollection = require( './collection' );
/**
* TODO: consider refactor this class.
* TODO: Rename to RevisionsModule.
*/
export default class RevisionsManager {
document;
revisions;
constructor( document ) {
this.document = document;
}
getItems() {
return this.revisions;
}
requestRevisions( callback ) {
if ( this.revisions ) {
callback( this.revisions );
return;
}
elementorCommon.ajax.addRequest( 'get_revisions', {
success: ( data ) => {
this.revisions = new RevisionsCollection( data );
this.revisions.on( 'update', this.onRevisionsUpdate.bind( this ) );
callback( this.revisions );
},
} );
}
setEditorData( data ) {
const collection = elementor.getPreviewView().collection;
collection.reset( data );
}
getRevisionDataAsync( id, options ) {
_.extend( options, {
data: {
id: id,
},
} );
return elementorCommon.ajax.addRequest( 'get_revision_data', options );
}
addRevisions( items ) {
this.requestRevisions( () => {
items.forEach( ( item ) => {
const existedModel = this.revisions.findWhere( {
id: item.id,
} );
if ( existedModel ) {
this.revisions.remove( existedModel, { silent: true } );
}
this.revisions.add( item, { silent: true } );
} );
this.revisions.trigger( 'update' );
} );
}
deleteRevision( revisionModel, options ) {
const params = {
data: {
id: revisionModel.get( 'id' ),
},
success: () => {
if ( options.success ) {
options.success();
}
revisionModel.destroy();
},
};
if ( options.error ) {
params.error = options.error;
}
elementorCommon.ajax.addRequest( 'delete_revision', params );
}
onRevisionsUpdate() {
if ( $e.routes.is( 'panel/history/revisions' ) ) {
$e.routes.refreshContainer( 'panel' );
}
}
}

View File

@@ -0,0 +1,9 @@
var RevisionModel;
RevisionModel = Backbone.Model.extend();
RevisionModel.prototype.sync = function() {
return null;
};
module.exports = RevisionModel;

View File

@@ -0,0 +1,5 @@
module.exports = Marionette.ItemView.extend( {
template: '#tmpl-elementor-panel-revisions-no-revisions',
id: 'elementor-panel-revisions-no-revisions',
className: 'elementor-nerd-box',
} );

View File

@@ -0,0 +1,15 @@
export default class extends Marionette.ItemView {
getTemplate() {
return '#tmpl-elementor-panel-revisions-loading';
}
id() {
return 'elementor-panel-revisions-loading';
}
onRender() {
this.options.document.revisions.requestRevisions( () => {
setTimeout( () => $e.routes.refreshContainer( 'panel' ) );
} );
}
}

View File

@@ -0,0 +1,208 @@
module.exports = Marionette.CompositeView.extend( {
id: 'elementor-panel-revisions',
template: '#tmpl-elementor-panel-revisions',
childView: require( './view' ),
childViewContainer: '#elementor-revisions-list',
ui: {
discard: '.elementor-panel-scheme-discard .elementor-button',
apply: '.elementor-panel-scheme-save .elementor-button',
},
events: {
'click @ui.discard': 'onDiscardClick',
'click @ui.apply': 'onApplyClick',
},
isRevisionApplied: false,
currentPreviewId: null,
currentPreviewItem: null,
document: null,
initialize: function( options ) {
this.document = options.document;
this.collection = this.document.revisions.getItems();
this.listenTo( elementor.channels.editor, 'saved', this.onEditorSaved );
this.currentPreviewId = elementor.config.document.revisions.current_id;
},
getRevisionViewData: function( revisionView ) {
this.document.revisions.getRevisionDataAsync( revisionView.model.get( 'id' ), {
success: ( data ) => {
if ( this.document.config.panel.has_elements ) {
this.document.revisions.setEditorData( data.elements );
}
elementor.settings.page.model.set( data.settings );
this.setRevisionsButtonsActive( true );
revisionView.$el.removeClass( 'elementor-revision-item-loading' );
this.enterReviewMode();
},
error: ( errorMessage ) => {
revisionView.$el.removeClass( 'elementor-revision-item-loading' );
this.currentPreviewItem = null;
this.currentPreviewId = null;
alert( errorMessage );
},
} );
},
setRevisionsButtonsActive: function( active ) {
// Check the tab is open.
if ( ! this.isDestroyed ) {
this.ui.apply.add( this.ui.discard ).prop( 'disabled', ! active );
}
},
deleteRevision: function( revisionView ) {
revisionView.$el.addClass( 'elementor-revision-item-loading' );
this.document.revisions.deleteRevision( revisionView.model, {
success: () => {
if ( revisionView.model.get( 'id' ) === this.currentPreviewId ) {
this.onDiscardClick();
}
this.currentPreviewId = null;
},
error: () => {
revisionView.$el.removeClass( 'elementor-revision-item-loading' );
alert( 'An error occurred' );
},
} );
},
enterReviewMode: function() {
elementor.changeEditMode( 'review' );
},
exitReviewMode: function() {
elementor.changeEditMode( 'edit' );
},
navigate: function( reverse ) {
if ( ! this.currentPreviewId || ! this.currentPreviewItem || this.children.length <= 1 ) {
return;
}
var currentPreviewItemIndex = this.collection.indexOf( this.currentPreviewItem.model ),
requiredIndex = reverse ? currentPreviewItemIndex - 1 : currentPreviewItemIndex + 1;
if ( requiredIndex < 0 ) {
requiredIndex = this.collection.length - 1;
}
if ( requiredIndex >= this.collection.length ) {
requiredIndex = 0;
}
this.children.findByIndex( requiredIndex ).ui.detailsArea.trigger( 'click' );
},
onEditorSaved: function() {
this.exitReviewMode();
this.setRevisionsButtonsActive( false );
this.currentPreviewId = elementor.config.document.revisions.current_id;
},
onApplyClick: function() {
$e.internal( 'document/save/set-is-modified', { status: true } );
$e.run( 'document/save/auto', { force: true } );
this.isRevisionApplied = true;
this.currentPreviewId = null;
this.document.history.getItems().reset();
},
onDiscardClick: function() {
if ( this.document.config.panel.has_elements ) {
this.document.revisions.setEditorData( elementor.config.document.elements );
}
$e.internal( 'document/save/set-is-modified', { status: this.isRevisionApplied } );
this.isRevisionApplied = false;
this.setRevisionsButtonsActive( false );
this.currentPreviewId = null;
this.exitReviewMode();
if ( this.currentPreviewItem ) {
this.currentPreviewItem.$el.removeClass( 'elementor-revision-current-preview' );
}
},
onDestroy: function() {
if ( this.currentPreviewId && this.currentPreviewId !== elementor.config.document.revisions.current_id ) {
this.onDiscardClick();
}
},
onRenderCollection: function() {
if ( ! this.currentPreviewId ) {
return;
}
var currentPreviewModel = this.collection.findWhere( { id: this.currentPreviewId } );
// Ensure the model is exist and not deleted during a save.
if ( currentPreviewModel ) {
this.currentPreviewItem = this.children.findByModelCid( currentPreviewModel.cid );
this.currentPreviewItem.$el.addClass( 'elementor-revision-current-preview' );
}
},
onChildviewDetailsAreaClick: function( childView ) {
const revisionID = childView.model.get( 'id' );
if ( revisionID === this.currentPreviewId ) {
return;
}
if ( this.currentPreviewItem ) {
this.currentPreviewItem.$el.removeClass( 'elementor-revision-current-preview elementor-revision-item-loading' );
}
childView.$el.addClass( 'elementor-revision-current-preview elementor-revision-item-loading' );
const revision = ( null === this.currentPreviewId || elementor.config.document.revisions.current_id === this.currentPreviewId );
if ( revision && elementor.saver.isEditorChanged() ) {
// TODO: Change to 'document/save/auto' ?.
$e.internal( 'document/save/save', {
status: 'autosave',
onSuccess: () => {
this.getRevisionViewData( childView );
},
} );
} else {
this.getRevisionViewData( childView );
}
this.currentPreviewItem = childView;
this.currentPreviewId = revisionID;
},
} );

View File

@@ -0,0 +1,13 @@
module.exports = Marionette.ItemView.extend( {
template: '#tmpl-elementor-panel-revisions-revision-item',
className: 'elementor-revision-item',
ui: {
detailsArea: '.elementor-revision-item__details',
},
triggers: {
'click @ui.detailsArea': 'detailsArea:click',
},
} );

View File

@@ -0,0 +1,91 @@
.elementor-history- {
&item {
display: flex;
align-items: center;
border: 1px solid $editor-lightest;
padding: 10px 15px;
margin-bottom: 10px;
font-size: 11px;
line-height: 1.4;
cursor: pointer;
transition: $transition-hover;
&:hover {
background-color: fade_out($editor-background, 0.7) ;
.elementor-history-item__icon{
.eicon:before {
content:'\e924';
}
}
}
&-applied {
color: $editor-light;
}
&-current {
background: $editor-background;
cursor: default;
.elementor-history-item__icon, &:hover .elementor-history-item__icon{
.eicon:before {
content:'\e90e';
}
}
}
&__details{
width: 95%;
}
&__title {
font-weight: bold;
}
&__subtitle, &__action {
font-weight: lighter;
}
&__action {
font-style: italic;
text-decoration: underline;
}
.__icon {
float: $end;
}
}
&revisions-message{
font-size: 11px;
text-align: center;
padding-top: 5px;
}
}
#elementor-panel-history {
padding: 20px 20px 15px;
&.elementor-empty {
.elementor-history-revisions-message {
padding-top: 20px;
}
}
&:not(.elementor-empty) {
background-color: #fff;
margin-top: 10px;
}
}
#elementor-panel-history-no-items,
#elementor-panel-revisions-no-revisions {
text-align: center;
.elementor-nerd-box-icon {
margin-top: 20px;
}
}

View File

@@ -0,0 +1,78 @@
.elementor-revision- {
&item {
&__wrapper {
display: flex;
align-items: center;
border: 1px solid $editor-lightest;
padding: 10px 15px;
margin-bottom: 10px;
font-size: 11px;
transition: $transition-hover;
&.current {
font-weight: bold;
}
.elementor-revision-item__tools-current {
color: $editor-info
}
}
&:hover:not(.elementor-revision-current-preview) {
background-color: fade_out($editor-background, 0.7) ;
}
&-loading {
.elementor-revision-item__tools-current {
display: none;
}
}
&:not(.elementor-revision-item-loading) {
.elementor-revision-item__tools-spinner {
display: none;
}
}
&__gravatar {
border-radius: 50%;
overflow: hidden;
img {
display: block;
}
}
&__details {
@include padding-start(15px);
flex-grow: 1;
cursor: pointer;
}
}
&meta {
padding-top: 5px;
font-size: 10px;
font-weight: bold;
}
&current-preview {
background-color: $editor-background;
}
}
#elementor-restore-autosave-dialog.dialog-widget {
background-color: rgba(0, 0, 0, 0.3);
}
#elementor-panel-revisions-loading {
@include absolute-center;
.eicon-loading {
font-size: 50px;
color: $editor-light;
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Elementor\Modules\History;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor history module.
*
* Elementor history module handler class is responsible for registering and
* managing Elementor history modules.
*
* @since 1.7.0
*/
class Module extends BaseModule {
/**
* Get module name.
*
* Retrieve the history module name.
*
* @since 1.7.0
* @access public
*
* @return string Module name.
*/
public function get_name() {
return 'history';
}
/**
* Localize settings.
*
* Add new localized settings for the history module.
*
* Fired by `elementor/editor/localize_settings` filter.
*
* @since 1.7.0
* @access public
* @deprecated 3.1.0
*
* @return array Localized settings.
*/
public function localize_settings() {
Plugin::$instance->modules_manager->get_modules( 'dev-tools' )->deprecation->deprecated_function( __METHOD__, '3.1.0' );
return [];
}
/**
* @since 2.3.0
* @access public
*/
public function add_templates() {
Plugin::$instance->common->add_template( __DIR__ . '/views/history-panel-template.php' );
Plugin::$instance->common->add_template( __DIR__ . '/views/revisions-panel-template.php' );
}
/**
* History module constructor.
*
* Initializing Elementor history module.
*
* @since 1.7.0
* @access public
*/
public function __construct() {
add_action( 'elementor/editor/init', [ $this, 'add_templates' ] );
}
}

View File

@@ -0,0 +1,421 @@
<?php
namespace Elementor\Modules\History;
use Elementor\Core\Base\Document;
use Elementor\Core\Common\Modules\Ajax\Module as Ajax;
use Elementor\Core\Files\CSS\Post as Post_CSS;
use Elementor\Plugin;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Elementor history revisions manager.
*
* Elementor history revisions manager handler class is responsible for
* registering and managing Elementor revisions manager.
*
* @since 1.7.0
*/
class Revisions_Manager {
/**
* Maximum number of revisions to display.
*/
const MAX_REVISIONS_TO_DISPLAY = 100;
/**
* Authors list.
*
* Holds all the authors.
*
* @access private
*
* @var array
*/
private static $authors = [];
/**
* History revisions manager constructor.
*
* Initializing Elementor history revisions manager.
*
* @since 1.7.0
* @access public
*/
public function __construct() {
self::register_actions();
}
/**
* @since 1.7.0
* @access public
* @static
*/
public static function handle_revision() {
add_filter( 'wp_save_post_revision_check_for_changes', '__return_false' );
}
/**
* @since 2.0.0
* @access public
* @static
*
* @param $post_content
* @param $post_id
*
* @return string
*/
public static function avoid_delete_auto_save( $post_content, $post_id ) {
// Add a temporary string in order the $post will not be equal to the $autosave
// in edit-form-advanced.php:210
$document = Plugin::$instance->documents->get( $post_id );
if ( $document && $document->is_built_with_elementor() ) {
$post_content .= '<!-- Created with Elementor -->';
}
return $post_content;
}
/**
* @since 2.0.0
* @access public
* @static
*/
public static function remove_temp_post_content() {
global $post;
$document = Plugin::$instance->documents->get( $post->ID );
if ( ! $document || ! $document->is_built_with_elementor() ) {
return;
}
$post->post_content = str_replace( '<!-- Created with Elementor -->', '', $post->post_content );
}
/**
* @since 1.7.0
* @access public
* @static
*
* @param int $post_id
* @param array $query_args
* @param bool $parse_result
*
* @return array
*/
public static function get_revisions( $post_id = 0, $query_args = [], $parse_result = true ) {
$post = get_post( $post_id );
if ( ! $post || empty( $post->ID ) ) {
return [];
}
$revisions = [];
$default_query_args = [
'posts_per_page' => self::MAX_REVISIONS_TO_DISPLAY,
'meta_key' => '_elementor_data',
];
$query_args = array_merge( $default_query_args, $query_args );
$posts = wp_get_post_revisions( $post->ID, $query_args );
if ( ! wp_revisions_enabled( $post ) ) {
$autosave = Utils::get_post_autosave( $post->ID );
if ( $autosave ) {
if ( $parse_result ) {
array_unshift( $posts, $autosave );
} else {
array_unshift( $posts, $autosave->ID );
}
}
}
if ( $parse_result ) {
array_unshift( $posts, $post );
} else {
array_unshift( $posts, $post->ID );
return $posts;
}
$current_time = current_time( 'timestamp' );
/** @var \WP_Post $revision */
foreach ( $posts as $revision ) {
$date = date_i18n( _x( 'M j @ H:i', 'revision date format', 'elementor' ), strtotime( $revision->post_modified ) );
$human_time = human_time_diff( strtotime( $revision->post_modified ), $current_time );
if ( $revision->ID === $post->ID ) {
$type = 'current';
$type_label = __( 'Current Version', 'elementor' );
} elseif ( false !== strpos( $revision->post_name, 'autosave' ) ) {
$type = 'autosave';
$type_label = __( 'Autosave', 'elementor' );
} else {
$type = 'revision';
$type_label = __( 'Revision', 'elementor' );
}
if ( ! isset( self::$authors[ $revision->post_author ] ) ) {
self::$authors[ $revision->post_author ] = [
'avatar' => get_avatar( $revision->post_author, 22 ),
'display_name' => get_the_author_meta( 'display_name', $revision->post_author ),
];
}
$revisions[] = [
'id' => $revision->ID,
'author' => self::$authors[ $revision->post_author ]['display_name'],
'timestamp' => strtotime( $revision->post_modified ),
'date' => sprintf(
/* translators: 1: Human readable time difference, 2: Date */
__( '%1$s ago (%2$s)', 'elementor' ),
$human_time,
$date
),
'type' => $type,
'typeLabel' => $type_label,
'gravatar' => self::$authors[ $revision->post_author ]['avatar'],
];
}
return $revisions;
}
/**
* @since 1.9.2
* @access public
* @static
*/
public static function update_autosave( $autosave_data ) {
self::save_revision( $autosave_data['ID'] );
}
/**
* @since 1.7.0
* @access public
* @static
*/
public static function save_revision( $revision_id ) {
$parent_id = wp_is_post_revision( $revision_id );
if ( $parent_id ) {
Plugin::$instance->db->safe_copy_elementor_meta( $parent_id, $revision_id );
}
}
/**
* @since 1.7.0
* @access public
* @static
*/
public static function restore_revision( $parent_id, $revision_id ) {
$parent = Plugin::$instance->documents->get( $parent_id );
$revision = Plugin::$instance->documents->get( $revision_id );
if ( ! $parent || ! $revision ) {
return;
}
$is_built_with_elementor = $revision->is_built_with_elementor();
$parent->set_is_built_with_elementor( $is_built_with_elementor );
if ( ! $is_built_with_elementor ) {
return;
}
Plugin::$instance->db->copy_elementor_meta( $revision_id, $parent_id );
$post_css = Post_CSS::create( $parent_id );
$post_css->update();
}
/**
* @since 2.3.0
* @access public
* @static
*
* @param $data
*
* @return array
* @throws \Exception
*/
public static function ajax_get_revision_data( array $data ) {
if ( ! isset( $data['id'] ) ) {
throw new \Exception( 'You must set the revision ID.' );
}
$revision = Plugin::$instance->documents->get( $data['id'] );
if ( ! $revision ) {
throw new \Exception( 'Invalid revision.' );
}
if ( ! current_user_can( 'edit_post', $revision->get_id() ) ) {
throw new \Exception( __( 'Access denied.', 'elementor' ) );
}
$revision_data = [
'settings' => $revision->get_settings(),
'elements' => $revision->get_elements_data(),
];
return $revision_data;
}
/**
* @since 1.7.0
* @access public
* @static
*/
public static function add_revision_support_for_all_post_types() {
$post_types = get_post_types_by_support( 'elementor' );
foreach ( $post_types as $post_type ) {
add_post_type_support( $post_type, 'revisions' );
}
}
/**
* @since 2.0.0
* @access public
* @static
* @param array $return_data
* @param Document $document
*
* @return array
*/
public static function on_ajax_save_builder_data( $return_data, $document ) {
$post_id = $document->get_main_id();
$latest_revisions = self::get_revisions(
$post_id, [
'posts_per_page' => 1,
]
);
$all_revision_ids = self::get_revisions(
$post_id, [
'fields' => 'ids',
], false
);
// Send revisions data only if has revisions.
if ( ! empty( $latest_revisions ) ) {
$current_revision_id = self::current_revision_id( $post_id );
$return_data = array_replace_recursive( $return_data, [
'config' => [
'document' => [
'revisions' => [
'current_id' => $current_revision_id,
],
],
],
'latest_revisions' => $latest_revisions,
'revisions_ids' => $all_revision_ids,
] );
}
return $return_data;
}
/**
* @since 1.7.0
* @access public
* @static
*/
public static function db_before_save( $status, $has_changes ) {
if ( $has_changes ) {
self::handle_revision();
}
}
public static function document_config( $settings, $post_id ) {
$settings['revisions'] = [
'enabled' => ( $post_id && wp_revisions_enabled( get_post( $post_id ) ) ),
'current_id' => self::current_revision_id( $post_id ),
];
return $settings;
}
/**
* Localize settings.
*
* Add new localized settings for the revisions manager.
*
* Fired by `elementor/editor/editor_settings` filter.
*
* @since 1.7.0
* @access public
* @static
* @deprecated 3.1.0
*/
public static function editor_settings() {
Plugin::$instance->modules_manager->get_modules( 'dev-tools' )->deprecation->deprecated_function( __METHOD__, '3.1.0' );
return [];
}
public static function ajax_get_revisions() {
return self::get_revisions();
}
/**
* @since 2.3.0
* @access public
* @static
*/
public static function register_ajax_actions( Ajax $ajax ) {
$ajax->register_ajax_action( 'get_revisions', [ __CLASS__, 'ajax_get_revisions' ] );
$ajax->register_ajax_action( 'get_revision_data', [ __CLASS__, 'ajax_get_revision_data' ] );
}
/**
* @since 1.7.0
* @access private
* @static
*/
private static function register_actions() {
add_action( 'wp_restore_post_revision', [ __CLASS__, 'restore_revision' ], 10, 2 );
add_action( 'init', [ __CLASS__, 'add_revision_support_for_all_post_types' ], 9999 );
add_filter( 'elementor/document/config', [ __CLASS__, 'document_config' ], 10, 2 );
add_action( 'elementor/db/before_save', [ __CLASS__, 'db_before_save' ], 10, 2 );
add_action( '_wp_put_post_revision', [ __CLASS__, 'save_revision' ] );
add_action( 'wp_creating_autosave', [ __CLASS__, 'update_autosave' ] );
add_action( 'elementor/ajax/register_actions', [ __CLASS__, 'register_ajax_actions' ] );
// Hack to avoid delete the auto-save revision in WP editor.
add_filter( 'edit_post_content', [ __CLASS__, 'avoid_delete_auto_save' ], 10, 2 );
add_action( 'edit_form_after_title', [ __CLASS__, 'remove_temp_post_content' ] );
if ( wp_doing_ajax() ) {
add_filter( 'elementor/documents/ajax_save/return_data', [ __CLASS__, 'on_ajax_save_builder_data' ], 10, 2 );
}
}
/**
* @since 1.9.0
* @access private
* @static
*/
private static function current_revision_id( $post_id ) {
$current_revision_id = $post_id;
$autosave = Utils::get_post_autosave( $post_id );
if ( is_object( $autosave ) ) {
$current_revision_id = $autosave->ID;
}
return $current_revision_id;
}
}

View File

@@ -0,0 +1,35 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<script type="text/template" id="tmpl-elementor-panel-history-page">
<div id="elementor-panel-elements-navigation" class="elementor-panel-navigation">
<div class="elementor-component-tab elementor-panel-navigation-tab" data-tab="actions"><?php echo __( 'Actions', 'elementor' ); ?></div>
<div class="elementor-component-tab elementor-panel-navigation-tab" data-tab="revisions"><?php echo __( 'Revisions', 'elementor' ); ?></div>
</div>
<div id="elementor-panel-history-content"></div>
</script>
<script type="text/template" id="tmpl-elementor-panel-history-tab">
<div id="elementor-history-list"></div>
<div class="elementor-history-revisions-message"><?php echo __( 'Switch to Revisions tab for older versions', 'elementor' ); ?></div>
</script>
<script type="text/template" id="tmpl-elementor-panel-history-no-items">
<img class="elementor-nerd-box-icon" src="<?php echo ELEMENTOR_ASSETS_URL . 'images/information.svg'; ?>" />
<div class="elementor-nerd-box-title"><?php echo __( 'No History Yet', 'elementor' ); ?></div>
<div class="elementor-nerd-box-message"><?php echo __( 'Once you start working, you\'ll be able to redo / undo any action you make in the editor.', 'elementor' ); ?></div>
</script>
<script type="text/template" id="tmpl-elementor-panel-history-item">
<div class="elementor-history-item__details">
<span class="elementor-history-item__title">{{{ title }}}</span>
<span class="elementor-history-item__subtitle">{{{ subTitle }}}</span>
<span class="elementor-history-item__action">{{{ action }}}</span>
</div>
<div class="elementor-history-item__icon">
<span class="eicon" aria-hidden="true"></span>
</div>
</script>

View File

@@ -0,0 +1,69 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
?>
<script type="text/template" id="tmpl-elementor-panel-revisions">
<div class="elementor-panel-box">
<div class="elementor-panel-scheme-buttons">
<div class="elementor-panel-scheme-button-wrapper elementor-panel-scheme-discard">
<button class="elementor-button" disabled>
<i class="eicon-close" aria-hidden="true"></i>
<?php echo __( 'Discard', 'elementor' ); ?>
</button>
</div>
<div class="elementor-panel-scheme-button-wrapper elementor-panel-scheme-save">
<button class="elementor-button elementor-button-success" disabled>
<?php echo __( 'Apply', 'elementor' ); ?>
</button>
</div>
</div>
</div>
<div class="elementor-panel-box">
<div class="elementor-panel-heading">
<div class="elementor-panel-heading-title"><?php echo __( 'Revisions', 'elementor' ); ?></div>
</div>
<div id="elementor-revisions-list" class="elementor-panel-box-content"></div>
</div>
</script>
<script type="text/template" id="tmpl-elementor-panel-revisions-no-revisions">
<#
var no_revisions_1 = '<?php echo __( 'Revision history lets you save your previous versions of your work, and restore them any time.', 'elementor' ); ?>',
no_revisions_2 = '<?php echo __( 'Start designing your page and you will be able to see the entire revision history here.', 'elementor' ); ?>',
revisions_disabled_1 = '<?php echo __( 'It looks like the post revision feature is unavailable in your website.', 'elementor' ); ?>',
revisions_disabled_2 = '<?php printf( __( 'Learn more about <a target="_blank" href="%s">WordPress revisions</a>', 'elementor' ), 'https://go.elementor.com/wordpress-revisions/' ); /* translators: %s: Codex URL */ ?>';
#>
<img class="elementor-nerd-box-icon" src="<?php echo ELEMENTOR_ASSETS_URL . 'images/information.svg'; ?>" />
<div class="elementor-nerd-box-title"><?php echo __( 'No Revisions Saved Yet', 'elementor' ); ?></div>
<div class="elementor-nerd-box-message">{{{ elementor.config.document.revisions.enabled ? no_revisions_1 : revisions_disabled_1 }}}</div>
<div class="elementor-nerd-box-message">{{{ elementor.config.document.revisions.enabled ? no_revisions_2 : revisions_disabled_2 }}}</div>
</script>
<script type="text/template" id="tmpl-elementor-panel-revisions-loading">
<i class="eicon-loading eicon-animation-spin" aria-hidden="true"></i>
</script>
<script type="text/template" id="tmpl-elementor-panel-revisions-revision-item">
<div class="elementor-revision-item__wrapper {{ type }}">
<div class="elementor-revision-item__gravatar">{{{ gravatar }}}</div>
<div class="elementor-revision-item__details">
<div class="elementor-revision-date" title="{{{ new Date( timestamp * 1000 ) }}}">{{{ date }}}</div>
<div class="elementor-revision-meta">
<span>{{{ typeLabel }}}</span>
<?php echo __( 'By', 'elementor' ); ?> {{{ author }}}
<span>(#{{{ id }}})</span>&nbsp;
</div>
</div>
<div class="elementor-revision-item__tools">
<# if ( 'current' === type ) { #>
<i class="elementor-revision-item__tools-current eicon-star" aria-hidden="true"></i>
<span class="elementor-screen-only"><?php echo __( 'Current', 'elementor' ); ?></span>
<# } #>
<i class="elementor-revision-item__tools-spinner eicon-loading eicon-animation-spin" aria-hidden="true"></i>
</div>
</div>
</script>