From dbbcd08d8fddb62b7b89a412dc9f2cac34d458bf Mon Sep 17 00:00:00 2001 From: Mateusz Uzdowski Date: Tue, 6 Nov 2012 16:14:30 +1300 Subject: [PATCH] NEW Extend the ssui.button with alternate appearances. Reusable feature for making CMS buttons that respond to the current contextual state with an appearance change. Provides capability to specify initial state, alternate icon and alternate text via data attributes or options (PHP or JS), and to hook up into events triggered when the state is alternating. This is used by the follow-up cms action buttons cleanup work. --- admin/javascript/ssui.core.js | 105 +++++++++++++++- docs/en/howto/cms-alternating-button.md | 160 ++++++++++++++++++++++++ 2 files changed, 261 insertions(+), 4 deletions(-) create mode 100644 docs/en/howto/cms-alternating-button.md diff --git a/admin/javascript/ssui.core.js b/admin/javascript/ssui.core.js index 758f13c18..105916c58 100644 --- a/admin/javascript/ssui.core.js +++ b/admin/javascript/ssui.core.js @@ -1,18 +1,115 @@ (function($) { /** - * Allows icon definition via HTML5 data attrs for easier handling in PHP + * Allows icon definition via HTML5 data attrs for easier handling in PHP. + * + * Adds an alternative appearance so we can toggle back and forth between them + * and register event handlers to add custom styling and behaviour. Example use + * is in the CMS with the saving buttons - depending on the page's state one of + * them will either say "Save draft" or "Saved", and will have different colour. */ $.widget('ssui.button', $.ui.button, { + options: { + alternate: { + icon: null, + text: null + }, + showingAlternate: false + }, + + /** + * Switch between the alternate appearances. + */ + toggleAlternate: function() { + if (this._trigger('ontogglealternate')===false) return; + + // Only switch to alternate if it has been enabled through options. + if (!this.options.alternate.icon && !this.options.alternate.text) return; + + this.options.showingAlternate = !this.options.showingAlternate; + this.refresh(); + }, + + /** + * Adjust the appearance to fit with the current settings. + */ + _refreshAlternate: function() { + this._trigger('beforerefreshalternate'); + + // Only switch to alternate if it has been enabled through options. + if (!this.options.alternate.icon && !this.options.alternate.text) return; + + if (this.options.showingAlternate) { + this.element.find('.ui-button-icon-primary').hide(); + this.element.find('.ui-button-text').hide(); + this.element.find('.ui-button-icon-alternate').show(); + this.element.find('.ui-button-text-alternate').show(); + } + else { + this.element.find('.ui-button-icon-primary').show(); + this.element.find('.ui-button-text').show(); + this.element.find('.ui-button-icon-alternate').hide(); + this.element.find('.ui-button-text-alternate').hide(); + } + + this._trigger('afterrefreshalternate'); + }, + + /** + * Construct button - pulls in options from data attributes. + * Injects new elements for alternate appearance (if requested via options). + */ _resetButton: function() { - var iconPrimary = this.element.data('iconPrimary') ? this.element.data('iconPrimary') : this.element.data('icon'), - iconSecondary = this.element.data('iconSecondary'); + var iconPrimary = this.element.data('icon-primary'), + iconSecondary = this.element.data('icon-secondary'); + + if (!iconPrimary) iconPrimary = this.element.data('icon'); + // TODO Move prefix out of this method, without requriing it for every icon definition in a data attr if(iconPrimary) this.options.icons.primary = 'btn-icon-' + iconPrimary; if(iconSecondary) this.options.icons.secondary = 'btn-icon-' + iconSecondary; $.ui.button.prototype._resetButton.call(this); - } + + // Pull options from data attributes. Overriden by explicit options given on widget creation. + if (!this.options.alternate.text) { + this.options.alternate.text = this.element.data('text-alternate'); + } + if (!this.options.alternate.icon) { + this.options.alternate.icon = this.element.data('icon-alternate'); + } + if (!this.options.showingAlternate) { + this.options.showingAlternate = this.element.hasClass('ss-ui-alternate'); + } + + // Create missing elements. + if (this.options.alternate.text) { + this.buttonElement.append( + "" + this.options.alternate.text + "" + ); + } + if (this.options.alternate.icon) { + this.buttonElement.append( + "" + ); + } + + this._refreshAlternate(); + }, + + refresh: function() { + $.ui.button.prototype.refresh.call(this); + + this._refreshAlternate(); + }, + + destroy: function() { + this.element.find('.ui-button-text-alternate').remove(); + this.element.find('.ui-button-icon-alternate').remove(); + + $.ui.button.prototype.destroy.call( this ); + }, }); /** diff --git a/docs/en/howto/cms-alternating-button.md b/docs/en/howto/cms-alternating-button.md new file mode 100644 index 000000000..7db08f80d --- /dev/null +++ b/docs/en/howto/cms-alternating-button.md @@ -0,0 +1,160 @@ +# How to implement an alternating button # + +## Introduction ## + +*Save* and *Save & publish* buttons alternate their appearance to reflect the state of the underlying `SiteTree` object. +This is based on a `ssui.button` extension available in `ssui.core.js`. + +The button can be configured via the data attributes in the backend, or through jQuery UI initialisation options. The +state can be toggled from the backend (again through data attributes), and can also be easily toggled or set on the +frontend. + +This how-to will walk you through creation of a "Clean-up" button with two appearances: + +* active: "Clean-up now" green constructive button if the actions can be performed +* netural: "Cleaned" default button if the action does not need to be done + +The controller code that goes with this example is listed in [Extend CMS Interface](../reference/extend-cms-interface). + +## Backend support ## + +First create and configure the action button with alternate state on a page type. The button comes with the default +state already, so you just need to add the alternate state using two data additional attributes: + +* `data-icon-alternate`: icon to be shown when the button is in the alternate state +* `data-text-alternate`: likewise for text. + +Here is the configuration code for the button: + + :::php + public function getCMSActions() { + $fields = parent::getCMSActions(); + + $fields->fieldByName('MajorActions')->push( + $cleanupAction = FormAction::create('cleanup', 'Cleaned') + // Set up an icon for the neutral state that will use the default text. + ->setAttribute('data-icon', 'accept') + // Initialise the alternate constructive state. + ->setAttribute('data-icon-alternate', 'addpage') + ->setAttribute('data-text-alternate', 'Clean-up now') + ); + + return $fields; + } + +You can control the state of the button from the backend by applying `ss-ui-alternate` class to the `FormAction`. To +simplify our example, let's assume the button state is controlled on the backend only, but you'd usually be better off +adjusting the state in the frontend to give the user the benefit of immediate feedback. This technique might still be +used for initialisation though. + +Here we initialise the button based on the backend check, and assume that the button will only update after page reload +(or on CMS action). + + :::php + public function getCMSActions() { + // ... + if ($this->needsCleaning()) { + // Will initialise the button into alternate state. + $cleanupAction->addExtraClass('ss-ui-alternate'); + } + // ... + } + +## Frontend support ## + +As with the *Save* and *Save & publish* buttons, you might want to add some scripted reactions to user actions on the +frontend. You can affect the state of the button through the jQuery UI calls. + +First of all, you can toggle the state of the button - execute this code in the browser's console to see how it works. + + :::js + jQuery('.cms-edit-form .Actions #Form_EditForm_action_cleanup').button('toggleAlternate'); + +Another, more useful, scenario is to check the current state. + + :::js + jQuery('.cms-edit-form .Actions #Form_EditForm_action_cleanup').button('option', 'showingAlternate'); + +You can also force the button into a specific state by using UI options. + + :::js + jQuery('.cms-edit-form .Actions #Form_EditForm_action_cleanup').button({showingAlternate: true}); + +This will allow you to react to user actions in the CMS and give immediate feedback. Here is an example taken from the +CMS core that tracks the changes to the input fields and reacts by enabling the *Save* and *Save & publish* buttons +(changetracker will automatically add `changed` class to the form if a modification is detected). + + :::js + /** + * Enable save buttons upon detecting changes to content. + * "changed" class is added by jQuery.changetracker. + */ + $('.cms-edit-form .changed').entwine({ + // This will execute when the class is added to the element. + onmatch: function(e) { + var form = this.closest('.cms-edit-form'); + form.find('#Form_EditForm_action_save').button({showingAlternate: true}); + form.find('#Form_EditForm_action_publish').button({showingAlternate: true}); + this._super(e); + }, + // Entwine requires us to define this, even if we don't use it. + onunmatch: function(e) { + this._super(e); + } + }); + +## Frontend hooks ## + +`ssui.button` defines several additional events so that you can extend the code with your own behaviours. For example +this is used in the CMS to style the buttons. Three events are available: + +* `ontogglealternate`: invoked when the `toggleAlternate` is called. Return `false` to prevent the toggling. +* `beforerefreshalternate`: invoked before the alternate-specific rendering takes place, including the button +initialisation. +* `afterrefreshalternate`: invoked after the rendering has been done, including on init. Good place to add styling +extras. + +Continuing our example let's add a "constructive" style to our *Clean-up* button. First you need to be able to add +custom JS code into the CMS. You can do this by adding a new source file, here +`mysite/javascript/CMSMain.CustomActionsExtension.js`, and requiring it from the config. + + :::ss + LeftAndMain::require_javascript('mysite/javascript/CMSMain.CustomActionsExtension.js'); + +You can now add the styling in response to `afterrefreshalternate` event. Let's use entwine to avoid accidental memory +leaks. The only complex part here is how the entwine handle is constructed. `onbuttonafterrefreshalternate` can be +disassembled into: + +* `on` signifies the entiwne event handler +* `button` is jQuery UI widget name +* `afterrefreshalternate`: the event from ssui.button to react to. + +Here is the entire handler put together. You don't need to add any separate initialisation code, this will handle all +cases. + + :::js + (function($) { + + $.entwine('mysite', function($){ + $('.cms-edit-form .Actions #Form_EditForm_action_cleanup').entwine({ + /** + * onafterrefreshalternate is SS-specific jQuery UI hook that is executed + * every time the button is rendered (including on initialisation). + */ + onbuttonafterrefreshalternate: function() { + if (this.button('option', 'showingAlternate')) { + this.addClass('ss-ui-action-constructive'); + } + else { + this.removeClass('ss-ui-action-constructive'); + } + } + }); + }); + + }(jQuery)); + +## Summary ## + +The code presented gives you a fully functioning alternating button, similar to the defaults that come with the the CMS. +These alternating buttons can be used to give user the advantage of visual feedback upon his actions.