pepebe
11/11/2014 - 11:08 AM

MODx (missing) preview of unsaved changes

MODx (missing) preview of unsaved changes

<?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;
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();
});

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.

Please let me know if this is working on your MODx installation: http://herooutoftime.com/modx-real-preview/