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.