From 2f2096cd3a4e2401a5b0a1634e4bb19e51c7abf0 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 9 Jun 2011 13:49:52 +1200 Subject: [PATCH] ENHANCEMENT HTML5 History.pushState support in CMS --- admin/code/LeftAndMain.php | 17 +++++++ admin/javascript/LeftAndMain.Content.js | 15 ++++--- admin/javascript/LeftAndMain.EditForm.js | 21 +++------ admin/javascript/LeftAndMain.Menu.js | 9 ++++ admin/javascript/LeftAndMain.Preview.js | 14 ++++-- admin/javascript/LeftAndMain.js | 57 +++++++++++++++++++++++- admin/javascript/ModelAdmin.History.js | 6 ++- 7 files changed, 113 insertions(+), 26 deletions(-) diff --git a/admin/code/LeftAndMain.php b/admin/code/LeftAndMain.php index d3548e44c..f0094cec3 100755 --- a/admin/code/LeftAndMain.php +++ b/admin/code/LeftAndMain.php @@ -69,6 +69,7 @@ class LeftAndMain extends Controller { */ static $allowed_actions = array( 'index', + 'save', 'savetreenode', 'getitem', 'getsubtree', @@ -241,6 +242,9 @@ class LeftAndMain extends Controller { Requirements::javascript(SAPPHIRE_ADMIN_DIR . '/thirdparty/jsizes/lib/jquery.sizes.js'); Requirements::javascript(SAPPHIRE_ADMIN_DIR . '/thirdparty/jlayout/lib/jlayout.border.js'); Requirements::javascript(SAPPHIRE_ADMIN_DIR . '/thirdparty/jlayout/lib/jquery.jlayout.js'); + Requirements::javascript(SAPPHIRE_ADMIN_DIR . '/thirdparty/history-js/scripts/uncompressed/history.js'); + Requirements::javascript(SAPPHIRE_ADMIN_DIR . '/thirdparty/history-js/scripts/uncompressed/history.html4.js'); + Requirements::javascript(SAPPHIRE_ADMIN_DIR . '/thirdparty/history-js/scripts/uncompressed/history.adapter.jquery.js'); Requirements::javascript(THIRDPARTY_DIR . '/behaviour/behaviour.js'); Requirements::javascript(THIRDPARTY_DIR . '/jquery-cookie/jquery.cookie.js'); @@ -300,6 +304,9 @@ class LeftAndMain extends Controller { SAPPHIRE_ADMIN_DIR . '/thirdparty/jsizes/lib/jquery.sizes.js', SAPPHIRE_ADMIN_DIR . '/thirdparty/jlayout/lib/jlayout.border.js', SAPPHIRE_ADMIN_DIR . '/thirdparty/jlayout/lib/jquery.jlayout.js', + SAPPHIRE_ADMIN_DIR . '/thirdparty/history-js/scripts/uncompressed/history.js', + SAPPHIRE_ADMIN_DIR . '/thirdparty/history-js/scripts/uncompressed/history.adapter.jquery.js', + SAPPHIRE_ADMIN_DIR . '/thirdparty/history-js/scripts/uncompressed/history.html4.js', THIRDPARTY_DIR . '/jstree/jquery.jstree.js', SAPPHIRE_ADMIN_DIR . '/javascript/jquery-changetracker/lib/jquery.changetracker.js', SAPPHIRE_DIR . '/javascript/TreeDropdownField.js', @@ -333,6 +340,16 @@ class LeftAndMain extends Controller { // TableListField.ss or Form.ss. SSViewer::set_theme(null); } + + function handleRequest($request) { + $response = parent::handleRequest($request); + $response->addHeader('X-Controller', $this->class); + return $response; + } + + function index($request) { + return ($this->isAjax()) ? $this->show($request) : $this->getViewer('index')->process($this); + } /** diff --git a/admin/javascript/LeftAndMain.Content.js b/admin/javascript/LeftAndMain.Content.js index 896eda0af..75c0e5095 100644 --- a/admin/javascript/LeftAndMain.Content.js +++ b/admin/javascript/LeftAndMain.Content.js @@ -20,11 +20,7 @@ var url = $(node).find('a:first').attr('href'); if(url && url != '#') { - var xmlhttp = self.loadForm( - url, - null, - function(response) {} - ); + window.History.pushState({}, '', url); } else { self.removeForm(); } @@ -33,11 +29,20 @@ this._super(); }, + onunmatch: function() { + this._super(); + }, + beforeLoad: function(url) { this.addClass('loading'); this.cleanup(); }, + afterLoad: function(data, status, xhr) { + this.removeClass('loading'); + this.replaceWith(data); + }, + cleanup: function() { this.empty(); }, diff --git a/admin/javascript/LeftAndMain.EditForm.js b/admin/javascript/LeftAndMain.EditForm.js index eac77c808..6c32940aa 100755 --- a/admin/javascript/LeftAndMain.EditForm.js +++ b/admin/javascript/LeftAndMain.EditForm.js @@ -79,6 +79,13 @@ $(this).remove(); }); + // Remove all TinyMCE instances + if((typeof tinymce != 'undefined') && tinymce.editors) { + $(tinymce.editors).each(function() { + if(typeof(this.remove) == 'function') this.remove(); + }); + } + this._super(); }, @@ -150,20 +157,6 @@ this.trigger('validate', {isValid: isValid}); return isValid; - }, - - /** - * Function: cleanup - * - * Remove all the currently active TinyMCE editors. - * Note: Everything that calls this externally has an inappropriate coupling to TinyMCE. - */ - cleanup: function() { - if((typeof tinymce != 'undefined') && tinymce.editors) { - $(tinymce.editors).each(function() { - if(typeof(this.remove) == 'function') this.remove(); - }); - } } }); diff --git a/admin/javascript/LeftAndMain.Menu.js b/admin/javascript/LeftAndMain.Menu.js index e7c09a287..25186a22a 100644 --- a/admin/javascript/LeftAndMain.Menu.js +++ b/admin/javascript/LeftAndMain.Menu.js @@ -25,11 +25,18 @@ */ $('.cms-menu-list').entwine({ onmatch: function() { + var self = this; + // TODO Fix icon etc. // this.children('li').each(function() { // $(this).find('a:first').append('o'); // }); + $('.LeftAndMain').bind('afterstatechange', function(e, data) { + var controller = data.xhr.getResponseHeader('X-Controller'); + if(controller) self.find('li#Menu-' + controller).select(); + }); + // Sync collapsed state with parent panel this.parents('.cms-panel:first').bind('toggle', function(e) { self.toggleClass('collapsed', $(this).hasClass('collapsed')); @@ -92,6 +99,8 @@ if(children.length) { children.first().find('a').click(); } else { + // Active menu item is set based on X-Controller ajax header, + // which matches one class on the menu window.History.pushState({}, '', this.attr('href')); } } diff --git a/admin/javascript/LeftAndMain.Preview.js b/admin/javascript/LeftAndMain.Preview.js index d4423c8d3..24eeb7790 100755 --- a/admin/javascript/LeftAndMain.Preview.js +++ b/admin/javascript/LeftAndMain.Preview.js @@ -36,6 +36,12 @@ var url = $(this).find(':input[name=StageURLSegment]').val(); if(url) self.loadUrl(url + '&cms-preview-disabled=1'); }); + + $('.cms-container').bind('afterstatechange', function(e) { + // var url = ui.xmlhttp.getResponseHeader('x-frontend-url'); + var url = $('.cms-edit-form').find(':input[name=StageURLSegment]').val(); + if(url) self.loadUrl(url + '&cms-preview-disabled=1'); + }); if(this.hasClass('is-expanded')) this.expand(); else this.collapse(); @@ -51,12 +57,14 @@ var doc = this.find('iframe')[0].contentDocument, container = this.getLayoutContainer(); // Only load if we're in the "edit page" view - if(!container.hasClass('CMSMain')) return; + if(!container.hasClass('CMSMain') || container.hasClass('CMSPagesController')) return; // Load this page in the admin interface if appropriate var id = $(doc).find('meta[name=x-page-id]').attr('content'), contentPanel = $('.cms-content'); // TODO Remove hardcoding - if(id && contentPanel.find(':input[name=ID]').val() != id) contentPanel.loadPanel('admin/page/edit/show/' + id); + if(id && contentPanel.find(':input[name=ID]').val() != id) { + window.History.pushState({}, '', 'admin/page/edit/show/' + id); + } }, _fixIframeLinks: function() { @@ -69,7 +77,7 @@ if (href && href.match(/^http:\/\//)) { links[i].setAttribute('href', 'javascript:false'); } else { - links[i].setAttribute('href', href + '?cms-preview=1'); + links[i].setAttribute('href', href + '?cms-preview-disabled=1'); } } }, diff --git a/admin/javascript/LeftAndMain.js b/admin/javascript/LeftAndMain.js index 794a46bdf..5da775475 100755 --- a/admin/javascript/LeftAndMain.js +++ b/admin/javascript/LeftAndMain.js @@ -41,13 +41,15 @@ * loadnewpage - ... */ $('.LeftAndMain').entwine({ - + + CurrentXHR: null, + /** * Constructor: onmatch */ onmatch: function() { var self = this; - + // Browser detection if($.browser.msie && parseInt($.browser.version, 10) < 7) { $('.ss-loading-screen').append( @@ -68,6 +70,10 @@ $(window).unbind('resize', positionLoadingSpinner); $('.cms-edit-form').live('loadnewpage', function() {self.redraw()}); + + History.Adapter.bind(window,'statechange',function(){ + self.handleStateChange(); + }); this._super(); }, @@ -77,6 +83,53 @@ var editForm = this.find('.cms-edit-form[data-layout]').layout(); this.find('.cms-content').layout(); this.find('.cms-container').layout({resize: false}) + }, + + /** + * Handles ajax loading of new panels through the window.History object. + * To trigger loading, pass a new URL to window.History.pushState(). + * + * Due to the nature of history management, no callbacks are allowed. + * Use the 'beforestatechange' and 'afterstatechange' events instead, + * or overwrite the beforeLoad() and afterLoad() methods on the + * DOM element you're loading the new content into. + * Although you can pass data into pushState(), it shouldn't contain + * DOM elements or callback closures. + * + * The passed URL should allow reconstructing important interface state + * without additional parameters, in the following use cases: + * - Explicit loading through History.pushState() + * - Implicit loading through browser navigation event triggered by the user (forward or back) + * - Full window refresh without ajax + * For example, a ModelAdmin search event should contain the search terms + * as URL parameters, and the result display should automatically appear + * if the URL is loaded without ajax. + * + * Alternatively, you can load new content via $('.cms-content').loadForm(). + * In this case, the action won't be recorded in the browser history. + */ + handleStateChange: function() { + var self = this, h = window.History, state = h.getState(); + + // Don't allow parallel loading to avoid edge cases + if(this.getCurrentXHR()) this.getCurrentXHR().abort(); + + // TODO Support loading into multiple panels + var contentEl = $(state.data.selector || '.LeftAndMain .cms-content'); + this.trigger('beforestatechange', {state: state}); + contentEl.beforeLoad(state.url); + + var xhr = $.ajax({ + url: state.url, + success: function(data, status, xhr) { + // Update panels + contentEl.afterLoad(data, status, xhr); + self.redraw(); + + self.trigger('afterstatechange', {data: data, status: status, xhr: xhr}); + } + }); + this.setCurrentXHR(xhr); } }); diff --git a/admin/javascript/ModelAdmin.History.js b/admin/javascript/ModelAdmin.History.js index ef249c6a1..177231b72 100755 --- a/admin/javascript/ModelAdmin.History.js +++ b/admin/javascript/ModelAdmin.History.js @@ -66,6 +66,8 @@ redraw: function() { this.find('.historyNav .forward').toggle(Boolean(this.getFuture().length > 0)); this.find('.historyNav .back').toggle(Boolean(this.getHistory().length > 1)); + + this._super(); }, /** @@ -116,7 +118,7 @@ this.trigger('historyGoBack', {url:previousPage}); // load new location - $('.cms-edit-form').loadForm(previousPage); + $('.cms-content').loadForm(previousPage); this.redraw(); } @@ -136,7 +138,7 @@ this.trigger('historyGoForward', {url:nextPage}); // load new location - $('.cms-edit-form').loadForm(nextPage); + $('.cms-content').loadForm(nextPage); this.redraw(); }