mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
Merge pull request #959 from silverstripe-droptables/alternating-button-rc
NEW Extend the ssui.button with alternate appearances.
This commit is contained in:
commit
87d538c7ee
@ -1,18 +1,115 @@
|
|||||||
(function($) {
|
(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, {
|
$.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() {
|
_resetButton: function() {
|
||||||
var iconPrimary = this.element.data('iconPrimary') ? this.element.data('iconPrimary') : this.element.data('icon'),
|
var iconPrimary = this.element.data('icon-primary'),
|
||||||
iconSecondary = this.element.data('iconSecondary');
|
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
|
// 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(iconPrimary) this.options.icons.primary = 'btn-icon-' + iconPrimary;
|
||||||
if(iconSecondary) this.options.icons.secondary = 'btn-icon-' + iconSecondary;
|
if(iconSecondary) this.options.icons.secondary = 'btn-icon-' + iconSecondary;
|
||||||
|
|
||||||
$.ui.button.prototype._resetButton.call(this);
|
$.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(
|
||||||
|
"<span class='ui-button-text-alternate ui-button-text'>" + this.options.alternate.text + "</span>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.options.alternate.icon) {
|
||||||
|
this.buttonElement.append(
|
||||||
|
"<span class='ui-button-icon-alternate ui-button-icon-primary ui-icon btn-icon-"
|
||||||
|
+ this.options.alternate.icon + "'></span>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 );
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
160
docs/en/howto/cms-alternating-button.md
Normal file
160
docs/en/howto/cms-alternating-button.md
Normal file
@ -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.
|
Loading…
x
Reference in New Issue
Block a user