Skip to content

Instantly share code, notes, and snippets.

@herooutoftime
Last active May 13, 2022 15:55
Show Gist options
  • Save herooutoftime/59eebf7c0bbf1299410c to your computer and use it in GitHub Desktop.
Save herooutoftime/59eebf7c0bbf1299410c to your computer and use it in GitHub Desktop.
MODx (missing) preview of unsaved changes

Idea & Concept

MODx Revolution lacks a preview of unsaved changes. For editors this is a rather important feature to check if their changes are correctly displayed and it's easy to do. No need to unpublish, save and view the resource. Just click 'Preview' and a MODx Window will show you all the changes.

The way it works is pretty easy: When clicking the 'Preview' button and OnBeforeDocFormSave is triggered all current (saved) data will be stored in a cache file, the resource will be saved with the new data. If OnWebPageComplete is fired, the saved data will be replaced with the previously cached data.

So for a short period the actual preview will be live!

Installation

  • Copy the contents of preview.plugin.php in to a plugin with these events:
    • OnDocFormRender
    • OnBeforeDocFormSave
    • OnDocFormSave
    • OnWebPageComplete
  • Create a JS file with the contents of preview.plugin.js somewhere (I prefer something like assets/js/plugins) in your MODx directory and replace it in preview.plugin.php
  • Edit one of your resources, change the title, content, ... and click on the 'Vorschau' button which should be displayed as the last action button on top.
  • Clicking on it should trigger a MODx Window and loading the resource with your changes you haven't saved yet.

Notice

Please test this on your local development before deploying it on any production server. It might not work as expected!
This is only tested in Chrome 38.0.2125.111 on MAC OS X 10.10 (Yosemite). There's an issue with MODx 2.3+: When previewing it will ask if you want to leave or stay on this page after clicking the button. Just stay and the MODx Window will pop up immediately after.

Ext.onReady(function () {
var ResourcePanel = Ext.getCmp('modx-panel-resource');
var ResourceTree = Ext.getCmp('modx-resource-tree');
var save_btn = Ext.getCmp('modx-abtn-save');
Previewr.window.Preview = function(config) {
config = config || {};
Ext.applyIf(config,{
title: 'Vorschau'
,closeAction: 'hide'
,width: 500
,height: 500
,maximized: true
,maximizable: false
,fields: [{
xtype : "component",
autoEl : {
tag : "iframe"
,width: '100%'
,height: '750px'
,border: '0'
,style: 'border:1px solid #ccc'
,src : Previewr.config.prev_url
}
}]
,buttons: [{
text: 'Schliessen'
,scope: this
,handler: function() { this.hide(); }
}]
});
Previewr.window.Preview.superclass.constructor.call(this,config);
}
Ext.extend(Previewr.window.Preview,MODx.Window);
Ext.reg('previewr-window-preview',Previewr.window.Preview);
/**
* [description]
* @param {[type]} form [description]
* @param {[type]} opt [description]
* @param {[type]} config [description]
* @return {[type]} [description]
*/
ResourcePanel.on('beforeSubmit', function(form, opt, config) {
if(ResourcePanel.getForm().findField("preview_check").getValue() == 0)
return;
var tp = Ext.getCmp('modx-leftbar-tabpanel');
var t = ResourceTree;
tp.activate('modx-resource-tree');
Ext.each(Previewr.config.all_parents, function(parent, index) {
var n = t.getNodeById('web_' + parent);
if(n)
n.expand();
});
if (save_btn)
save_btn.disable();
});
/**
* [description]
* @param {[type]} o [description]
* @return {[type]} [description]
*/
ResourcePanel.on('success', function(o) {
if(ResourcePanel.getForm().findField("preview_check").getValue() == 0)
return;
var g = Ext.getCmp('modx-grid-resource-security');
var t = ResourceTree;
// var save_btn = Ext.getCmp('modx-abtn-save');
if (g) {
g.getStore().commitChanges();
}
if (t) {
Ext.each(Previewr.config.all_parents, function(parent, index) {
var n = t.getNodeById('web_' + parent);
if(n)
n.expand();
});
var ctx = Ext.getCmp('modx-resource-context-key').getValue();
var pa = Ext.getCmp('modx-resource-parent-hidden').getValue();
var pao = Ext.getCmp('modx-resource-parent-old-hidden').getValue();
var n = t.getNodeById(ctx+'_'+pa);
if(pa !== pao) {
Ext.getCmp('modx-resource-parent-old-hidden').setValue(pa);
} else {
if(typeof n !== 'undefined')
n.leaf = false;
}
}
var object = o.result.object;
// object.parent is undefined on template changing.
if (this.config.resource && object.parent !== undefined && (object.class_key != this.defaultClassKey || object.parent != this.defaultValues.parent)) {
MODx.loadPage(location.href);
} else {
this.getForm().setValues(object);
Ext.getCmp('modx-page-update-resource').config.preview_url = object.preview_url;
}
ResourcePanel.fireEvent('fieldChange');
ResourcePanel.markDirty();
if (save_btn) {
save_btn.enable();
}
// Display a MODx Window with the preview
if(o.result.object.preview_check == 1) {
var updateWindow = MODx.load({
xtype: 'previewr-window-preview'
,title: 'Vorschau: ' + ResourcePanel.getForm().findField('pagetitle').getValue()
});
updateWindow.show();
setTimeout(function() { ResourcePanel.markDirty(); }, 2000);
}
if(o.result.object.action == 'update')
return;
this.getForm().setValues(o.result.object);
});
var uri = ResourcePanel.getForm().findField("uri").getValue();
var alias = ResourcePanel.getForm().findField("alias").getValue();
var id = ResourcePanel.getForm().findField("id").getValue();
// add hidden field to check if it is a preview-process on save
ResourcePanel.add({
xtype: 'hidden'
,name: 'preview_check'
,id: 'preview_check'
},{
xtype: 'hidden'
,name: 'preview_url'
,id: 'preview_url'
});
if(!save_btn)
return;
// If the resource hasn't been created yet it's not possible to generate a preview
if(save_btn.process == 'create')
return;
//reset preview_check when clicking the save-button
save_btn.on('click', function() {
ResourcePanel.getForm().findField("preview_check").setValue('0');
});
var modab = Ext.getCmp("modx-action-buttons");
// modab.add('-');
modab.add('-',{
xtype: 'button'
,text: 'Vorschau'
,id: 'modx-abtn-real-preview'
,method: 'remote'
,process: Previewr.config.update_processor
// ,checkDirty: true
,listeners: {
click: function(btn) {
//set prview_check to 1 when clicking the preview-button
ResourcePanel.getForm().findField("preview_check").setValue('1');
}
}
});
modab.doLayout();
});
<?php
/**
* Plugin: Previewer
* Delivers a preview of the current input.
* It uses the basic saving-process, but the old data is saved into an cachefile and resaved after the preview was regenerated.
*/
$event = $modx->event->name;
// Set cache options
$cacheOptions = array(
xPDO::OPT_CACHE_KEY => '',
xPDO::OPT_CACHE_HANDLER => 'xPDOFileCache',
xPDO::OPT_CACHE_EXPIRES => 0,
);
switch($event) {
case 'OnDocFormRender':
$parents = array_reverse($modx->getParentIds($id, 10, array('context' => 'web')));
if(in_array(33, $parents)) {
$js =<<<JS
<script type="text/javascript">
Ext.onReady(function() {
Ext.getCmp('modx-abtn-preview').getEl().hide();
})
</script>
JS;
$modx->regClientStartupHTMLBlock($js);
return;
}
// Only when resource exists already!
$has_duplicate = false;
if($mode == 'upd') {
$prevUrl = $modx->makeUrl($id, 'web', array('is_preview' => 1), 'full');
$allParents = json_encode($parents);
$_POST['id'] = $prev;
$modx_version = $modx->getVersionData();
$update_processor = $modx_version['major_version'] > 2 ? 'resource/update' : 'update';
$js =<<<JS
<script type="text/javascript">
var Previewr = function(config) {
config = config || {};
Previewr.superclass.constructor.call(this,config);
};
Ext.extend(Previewr, Ext.Component, {
page:{},window:{},grid:{},tree:{},panel:{},combo:{},config: {}
});
var Previewr = new Previewr();
Previewr.config = {
prev_url: '$prevUrl',
all_parents: '$allParents',
update_processor: '$update_processor'
}
</script>
JS;
$modx->regClientStartupHTMLBlock($js);
// Change this to the path to the JS-file in your installation
$modx->regClientStartupScript($modx->getOption('assets_url'). 'js/plugins/previewr.plugin.js');
}
$btn_js =<<<BTN_JS
<script type="text/javascript">
Ext.onReady(function () {
window.setInterval(function() {
// Always enable the save button
if(!Ext.getCmp('modx-abtn-save'))
return;
Ext.getCmp('modx-abtn-save').enable().setDisabled(false);
}, 2000);
});
</script>
BTN_JS;
$modx->regClientStartupHTMLBlock($btn_js);
break;
case 'OnBeforeDocFormSave':
// Check if it's a preview-process
if($_POST['preview_check'] != 1)
return;
// Get the resource
$resource = $modx->getObject('modResource', $id);
if(!$resource)
return;
// Add resource to the currently previewed resources
$current_res = $modx->cacheManager->get('preview/current_resources', $cacheOptions);
$current_res['resources'][] = $id;
$modx->cacheManager->set('preview/current_resources', $current_res, 60, $cacheOptions);
// Get all resource-data
$temp['resource'] = $resource->toArray();
// Get all TV-data
if ($tvs = $resource->getMany('TemplateVars', 'all')) {
foreach ($tvs as $tv) {
$temp_tvs[] = $tv->toArray();
}
}
$temp['tvs'] = $temp_tvs;
// Get all resource-groups
$groups = $resource->getMany('ResourceGroupResources');
foreach($groups as $name => $grpObject) {
$temp_groups[] = $grpObject->toArray();
}
$temp['groups'] = $temp_groups;
// Write all data (resource,tv,groups) to a cachefile
$modx->cacheManager->set('preview/'.$id.'.temp', $temp, 0, $cacheOptions);
// Log preview-process
$modx->logManagerAction('resource_preview', 'modResource', $id);
return;
break;
case 'OnDocFormSave':
// Exit if it's a preview process
if($_POST['preview_check'] == 1)
return;
function generateRandomString($length = 10) {
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[rand(0, strlen($characters) - 1)];
}
return $randomString;
}
if($id) {
$url = $modx->makeUrl($id, 'web', null, 'full');
//use parameter 'nc' to prevent retrieving a cached resource
$opts = array(
'http'=>array(
'method'=>"POST",
'content'=>http_build_query(array('t'=> generateRandomString())),
'timeout' => 0.1
)
);
$context = stream_context_create($opts);
$fp = @fopen($url, 'r', false, $context);
}
return;
break;
case 'OnWebPageComplete':
// Return if it is not a preview-process
if(!$_GET['is_preview'])
return;
// Get old resource cache
$data = $modx->cacheManager->get('preview/'.$modx->resource->get('id').'.temp', $cacheOptions);
// Remove cache file
$modx->cacheManager->clearCache(array('preview/'), array('objects' => null, 'extensions' => array('.temp.cache.php')));
// Set resource data
$modx->resource->fromArray($data['resource']);
// Set TVs
foreach($data['tvs'] as $tv) {
$modx->resource->setTVValue($tv['id'], $tv['value']);
}
// Set resource groups
foreach($data['groups'] as $group) {
$modx->resource->joinGroup($group['document_group']);
}
// Save the resource
$modx->resource->save();
// Remove the page from current preview-resources
$current_res = $modx->cacheManager->get('preview/current_resources', $cacheOptions);
foreach (array_keys($current_res['resources'], $modx->resource->get('id'), true) as $key) {
unset($current_res['resources'][$key]);
}
$modx->cacheManager->set('preview/current_resources', $current_res, 60, $cacheOptions);
return;
break;
}
return;
@patrickatwsrn
Copy link

Hi, the domain mentioned above shows a particular type of spicy content. I guess you might want to remove it from readme section.

@herooutoftime
Copy link
Author

Hi, the domain mentioned above shows a particular type of spicy content. I guess you might want to remove it from readme section.

Oops... Thank you @patrickatwsrn

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment