jquery.confirmable: New inline confirmation module

$().confirmable() can be applied to any inline-block element. It will
cause it to expand into "Are you sure? [Yes] [No]" question and
buttons when clicked, where the buttons are clones of the original
element: [Yes] will carry out the default action (or a different one,
if specified), [No] will collapse the interface back.

Possible uses include:
* Confirmable "rollback" links
* Confirmable "unwatch" links on watchlists
* Confirmable "thank" links (Echo extension's ones)

Added a demo with possible uses on history and watchlist pages.
Included Hebrew messages courtesy of Moriel.

Change-Id: I2f6e0bd4f6f0a84e1a0d7193cde076738f3cdd25
This commit is contained in:
Bartosz Dziewoński 2013-10-28 15:56:30 +01:00 committed by Legoktm
parent 02936f3d95
commit 13a14c70c6
9 changed files with 379 additions and 0 deletions

View file

@ -0,0 +1,147 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<!--
The jquery.confirmable module uses some additional modules and files
for internationalization support. These are omitted here for simplicity.
-->
<script type="text/javascript" src="../../resources/lib/jquery/jquery.js"></script>
<link rel="stylesheet" href="../../resources/src/jquery/jquery.confirmable.css">
<script type="text/javascript" src="../../resources/src/jquery/jquery.confirmable.js"></script>
<style>
body {
font: small sans-serif;
}
.mw-rollback-link a,
.mw-unwatch-link a,
.mw-thanks-thank-link a {
background: #ccf;
}
</style>
</head>
<body>
<h2>Introduction</h2>
<p>The jquery.confirmable module provides a simple inline confirmation script for potentially destructive or uncancellable actions.</p>
<p>Possible uses include confirmable "rollback" links in histories, confirmable "unwatch" links on watchlists, or confirmable "thanks" links (provided by the Echo extension).</p>
<p>Shown below is a demo of how each of those could work on history and watchlist entries, in an LTR and RTL language. The enhanced links are highlighted in blue.</p>
<h2>Examples</h2>
<h3>LTR (English)</h3>
<p>Watchlist:</p>
<ul lang="en" dir="ltr">
<li class="mw-line-even mw-changeslist-line-not-watched">
(<a href="#">diff</a> | <a href="#">hist</a>)
<span class="mw-changeslist-separator">. .</span>
<span class="mw-title"><a href="#" class="mw-changeslist-title">Example page</a></span>; <span class="mw-changeslist-date">13:38</span>
<span class="mw-changeslist-separator">. .</span>
<span class="mw-plusminus-neg">(-130)</span>
<span class="mw-changeslist-separator">. .</span>
<a href="#" class="mw-userlink">Example user</a>
<span class="mw-usertoollinks">(<a href="#">Talk</a> | <a href="#">contribs</a> | <a href="#">block</a>)</span>
<span class="comment">(example edit)</span>
<span class="mw-rollback-link">[<a href="https://www.mediawiki.org/wiki/Random_ideas_for_rollback_to_be_shelved_and_forgotten_about">rollback</a>]</span>
(<span class="mw-unwatch-link"><a href="#">unwatch</a></span>)
</li>
</ul>
<p>History:</p>
<ul lang="en" dir="ltr">
<li>
<span class="mw-history-histlinks">(cur | <a href="#">prev</a>)</span>
<input type="radio" style="visibility: hidden;" /><input type="radio" checked />
<a href="#" class="mw-changeslist-date">13:38, 28 October 2013</a>
<span class='history-user'>
<a href="#" class="mw-userlink">Example user</a>
<span class="mw-usertoollinks">(<a href="#">Talk</a> | <a href="#">contribs</a> | <a href="#">block</a>)</span>
</span>
<span class="mw-changeslist-separator">. .</span>
<span class="history-size">(1,654 bytes)</span>
<span class="mw-plusminus-neg">(-130)</span>
<span class="mw-changeslist-separator">. .</span>
<span class="comment">(example edit)</span>
(<span class="mw-rollback-link"><a href="https://www.mediawiki.org/wiki/Random_ideas_for_rollback_to_be_shelved_and_forgotten_about">rollback 1 edit</a></span> | <span class="mw-history-undo"><a href="#">undo</a></span> | <span class="mw-thanks-thank-link"><a href="#">thank</a></span>)
</li>
</ul>
<script type="text/javascript">
$( 'ul[lang="en"] .mw-rollback-link a' )
.confirmable({ i18n: { confirm: 'Are you sure you want to rollback?' } });
$( 'ul[lang="en"] .mw-unwatch-link a' )
.confirmable({ handler: function(){ alert('Unwatched!') } });
$( 'ul[lang="en"] .mw-thanks-thank-link a' )
.confirmable({ handler: function(){ alert('Thanked!') } });
</script>
<h3>RTL (Hebrew)</h3>
<!-- All of the Hebrew text below has been basically pulled out of my hat. -->
<p>Watchlist:</p>
<ul lang="he" dir="rtl">
<li class="mw-line-even mw-changeslist-line-not-watched">
(<a href="#">הבדל</a> | <a href="#">היסטוריה</a>)
<span class="mw-changeslist-separator">. .</span>
<span class="mw-title"><a href="#" class="mw-changeslist-title">דף דוגמה</a></span>; <span class="mw-changeslist-date">13:38</span>
<span class="mw-changeslist-separator">. .</span>
<span class="mw-plusminus-neg">(-57)</span>
<span class="mw-changeslist-separator">. .</span>
<a href="#" class="mw-userlink">דוגמא אדם</a>
<span class="mw-usertoollinks">(<a href="#">שיחה</a> | <a href="#">תרומות</a> | <a href="#">חסימה</a>)</span>
<span class="comment">(עריכה לדוגמה)</span>
<span class="mw-rollback-link">[<a href="https://www.mediawiki.org/wiki/Random_ideas_for_rollback_to_be_shelved_and_forgotten_about">שחזור</a>]</span>
(<span class="mw-unwatch-link"><a href="#">הפסקת מעקב</a></span>)
</li>
</ul>
<p>History:</p>
<ul lang="he" dir="rtl">
<li>
<span class="mw-history-histlinks">(נוכחית | <a href="#">קודמת</a>)</span>
<input type="radio" style="visibility: hidden;" /><input type="radio" checked />
<a href="#" class="mw-changeslist-date">23:41, 12 במאי 2012</a>
<span class='history-user'>
<a href="#" class="mw-userlink">דוגמא אדם</a>
<span class="mw-usertoollinks">(<a href="#">שיחה</a> | <a href="#">תרומות</a> | <a href="#">חסימה</a>)</span>
</span>
<span class="mw-changeslist-separator">. .</span>
<span class="history-size">(1,762 בתים)</span>
<span class="mw-plusminus-neg">(-57)</span>
<span class="mw-changeslist-separator">. .</span>
<span class="comment">(עריכה לדוגמה)</span>
(<span class="mw-rollback-link"><a href="https://www.mediawiki.org/wiki/Random_ideas_for_rollback_to_be_shelved_and_forgotten_about">שחזור עריכה אחת</a></span> | <span class="mw-history-undo"><a href="#">ביטול</a></span> | <span class="mw-thanks-thank-link"><a href="#">תודה</a></span>)
</li>
</ul>
<script type="text/javascript">
var hebrewI18n = {
confirm: 'האם ברצונך להמשיך?',
yes: 'כן',
no: 'לא',
}
$( 'ul[lang="he"] .mw-rollback-link a' )
.confirmable({ i18n: $.extend( {}, hebrewI18n, { confirm: 'האם ברצונך לשחזר?' } ) });
$( 'ul[lang="he"] .mw-unwatch-link a' )
.confirmable({ i18n: hebrewI18n, handler: function(){ alert('Unwatched!') } });
$( 'ul[lang="he"] .mw-thanks-thank-link a' )
.confirmable({ i18n: hebrewI18n, handler: function(){ alert('Thanked!') } });
</script>
<style type="text/css">
/* This is normally handled by CSSJanus. */
ul[dir=rtl] .jquery-confirmable-button {
margin-left: 0;
margin-right: 1ex;
}
</style>
</body>
</html>

View file

@ -267,6 +267,9 @@
"hidetoc": "hide",
"collapsible-collapse": "Collapse",
"collapsible-expand": "Expand",
"confirmable-confirm": "Are {{GENDER:$1|you}} sure?",
"confirmable-yes": "Yes",
"confirmable-no": "No",
"thisisdeleted": "View or restore $1?",
"viewdeleted": "View $1?",
"restorelink": "{{PLURAL:$1|one deleted edit|$1 deleted edits}}",

View file

@ -283,6 +283,9 @@
"hidetoc": "הסתרה",
"collapsible-collapse": "הסתרה",
"collapsible-expand": "הצגה",
"confirmable-confirm": "האם {{GENDER:$1|ברצונך}} להמשיך?",
"confirmable-yes": "כן",
"confirmable-no": "לא",
"thisisdeleted": "לשחזר או להציג $1?",
"viewdeleted": "להציג $1?",
"restorelink": "{{PLURAL:$1|גרסה מחוקה אחת|$1 גרסאות מחוקות}}",

View file

@ -429,6 +429,9 @@
"hidetoc": "This is the link used to hide the table of contents\n\n{{Identical|Hide}}",
"collapsible-collapse": "{{Doc-actionlink}}\nThis is the link used to collapse a collapsible element. (used as plaintext. No wikitext or html is parsed.)\n\nSee also:\n* {{msg-mw|Collapsible-expand}}\n{{Identical|Collapse}}",
"collapsible-expand": "{{Doc-actionlink}}\nThis is the link used to expand a collapsible element (used as plaintext. No wikitext or html is parsed.)\n\nSee also:\n* {{msg-mw|Collapsible-collapse}}\n\nSee the following example:\n{{Identical|Expand}}",
"confirmable-confirm": "Question asking the user to confirm a potentially uncancellable action.\n\"Yes\" and \"No\" buttons are displayed beside it.\n\nSee also:\n* {{msg-mw|confirmable-yes}}\n* {{msg-mw|confirmable-no}}\n",
"confirmable-yes": "{{Doc-actionlink}}\nText of a button that will confirm triggering of a potentially uncancellable action.\n\nSee also:\n* {{msg-mw|confirmable-confirm}}\n* {{msg-mw|confirmable-no}}",
"confirmable-no": "{{Doc-actionlink}}\nText of a button that will cancel triggering of a potentially uncancellable action.\n\nSee also:\n* {{msg-mw|confirmable-confirm}}\n* {{msg-mw|confirmable-yes}}",
"thisisdeleted": "Message shown on a deleted page when the user has the undelete right. Parameters:\n* $1 - a link to [[Special:Undelete]], with {{msg-mw|restorelink}} as the text\nSee also:\n* {{msg-mw|viewdeleted}}",
"viewdeleted": "Message shown on a deleted page when the user does not have the undelete right (but has the deletedhistory right).\n\nParameters:\n* $1 - a link to [[Special:Undelete]], with {{msg-mw|restorelink}} as the text\nSee also:\n* {{msg-mw|thisisdeleted}}",
"restorelink": "This text is always displayed in conjunction with the {{msg-mw|thisisdeleted}} message (View or restore $1?). The user will see\nView or restore <nowiki>{{PLURAL:$1|one deleted edit|$1 deleted edits}}</nowiki>? i.e ''View or restore one deleted edit?'' or\n''View or restore n deleted edits?''",

View file

@ -26,6 +26,7 @@
"../../resources/src/jquery/jquery.checkboxShiftClick.js",
"../../resources/src/jquery/jquery.client.js",
"../../resources/src/jquery/jquery.colorUtil.js",
"../../resources/src/jquery/jquery.confirmable.js",
"../../resources/src/jquery/jquery.footHovzer.js",
"../../resources/src/jquery/jquery.getAttrs.js",
"../../resources/src/jquery/jquery.hidpi.js",

View file

@ -191,6 +191,19 @@ return array(
'jquery.colorUtil' => array(
'scripts' => 'resources/src/jquery/jquery.colorUtil.js',
),
'jquery.confirmable' => array(
'scripts' => array(
'resources/src/jquery/jquery.confirmable.js',
'resources/src/jquery/jquery.confirmable.mediawiki.js',
),
'messages' => array(
'confirmable-confirm',
'confirmable-yes',
'confirmable-no',
),
'styles' => 'resources/src/jquery/jquery.confirmable.css',
'dependencies' => 'mediawiki.jqueryMsg',
),
// Use mediawiki.cookie in new code, rather than jquery.cookie.
'jquery.cookie' => array(
'scripts' => 'resources/lib/jquery/jquery.cookie.js',

View file

@ -0,0 +1,28 @@
.jquery-confirmable-button {
/* Automatically flipped */
margin-left: 1ex;
}
.jquery-confirmable-wrapper {
/* Line breaks within the interface text are unpleasant */
white-space: nowrap;
/* Hiding the original text when it slides to the left */
overflow: hidden;
}
.jquery-confirmable-wrapper,
.jquery-confirmable-element,
.jquery-confirmable-interface {
/* We need inline-block to be able to size the elements and calculate their dimensions */
display: inline-block;
/* inline-block elements in this context align to baseline by default */
vertical-align: bottom;
}
.jquery-confirmable-element {
transition: margin 250ms cubic-bezier(0.2, 0.8, 0.2, 0.8);
}
.jquery-confirmable-interface {
transition: width 250ms cubic-bezier(0.2, 0.8, 0.2, 0.8);
}

View file

@ -0,0 +1,168 @@
/**
* jQuery confirmable plugin
*
* Released under the MIT License.
*
* @author Bartosz Dziewoński
*
* @class jQuery.plugin.confirmable
*/
( function ( $ ) {
var identity = function ( data ) {
return data;
};
/**
* Enable inline confirmation for given clickable element (like `<a />` or `<button />`).
*
* An additional inline confirmation step being shown before the default action is carried out on
* click.
*
* Calling `.confirmable( { handler: function () { … } } )` will fire the handler only after the
* confirmation step.
*
* The element will have the `jquery-confirmable-element` class added to it when it's clicked for
* the first time, which has `white-space: nowrap;` and `display: inline-block;` defined in CSS.
* If the computed values for the element are different when you make it confirmable, you might
* encounter unexpected behavior.
*
* @param {Object} [options]
* @param {string} [options.events='click'] Events to hook to.
* @param {Function} [options.wrapperCallback] Callback to fire when preparing confirmable
* interface. Receives the interface jQuery object as the only parameter.
* @param {Function} [options.buttonCallback] Callback to fire when preparing confirmable buttons.
* It is fired separately for the 'Yes' and 'No' button. Receives the button jQuery object as
* the first parameter and 'yes' or 'no' as the second.
* @param {Function} [options.handler] Callback to fire when the action is confirmed (user clicks
* the 'Yes' button).
* @param {string} [options.i18n] Text to use for interface elements.
* @param {string} [options.i18n.confirm] Text to use for the confirmation question.
* @param {string} [options.i18n.yes] Text to use for the 'Yes' button.
* @param {string} [options.i18n.no] Text to use for the 'No' button.
*
* @chainable
*/
$.fn.confirmable = function ( options ) {
options = $.extend( true, {}, $.fn.confirmable.defaultOptions, options || {} );
return this.on( options.events, function ( e ) {
var $element, $text, $buttonYes, $buttonNo, $wrapper, $interface, $elementClone,
interfaceWidth, elementWidth, rtl, positionOffscreen, positionRestore, sideMargin;
$element = $( this );
if ( $element.data( 'jquery-confirmable-button' ) ) {
// We're running on a clone of this element that represents the 'Yes' or 'No' button.
// (This should never happen for the 'No' case unless calling code does bad things.)
return;
}
// Only prevent native event handling. Stopping other JavaScript event handlers
// is impossible because they might have already run (we have no control over the order).
e.preventDefault();
rtl = $element.css( 'direction' ) === 'rtl';
if ( rtl ) {
positionOffscreen = { position: 'absolute', right: '-9999px' };
positionRestore = { position: '', right: '' };
sideMargin = 'marginRight';
} else {
positionOffscreen = { position: 'absolute', left: '-9999px' };
positionRestore = { position: '', left: '' };
sideMargin = 'marginLeft';
}
if ( $element.hasClass( 'jquery-confirmable-element' ) ) {
$wrapper = $element.closest( '.jquery-confirmable-wrapper' );
$interface = $wrapper.find( '.jquery-confirmable-interface' );
$text = $interface.find( '.jquery-confirmable-text' );
$buttonYes = $interface.find( '.jquery-confirmable-button-yes' );
$buttonNo = $interface.find( '.jquery-confirmable-button-no' );
interfaceWidth = $interface.data( 'jquery-confirmable-width' );
elementWidth = $element.data( 'jquery-confirmable-width' );
} else {
$elementClone = $element.clone( true );
$element.addClass( 'jquery-confirmable-element' );
elementWidth = $element.width();
$element.data( 'jquery-confirmable-width', elementWidth );
$wrapper = $( '<span>' )
.addClass( 'jquery-confirmable-wrapper' );
$element.wrap( $wrapper );
// Build the mini-dialog
$text = $( '<span>' )
.addClass( 'jquery-confirmable-text' )
.text( options.i18n.confirm );
// Clone original element along with event handlers to easily replicate its behavior.
// We could fiddle with .trigger() etc., but that is troublesome especially since
// Safari doesn't implement .click() on <a> links and jQuery follows suit.
$buttonYes = $elementClone.clone( true )
.addClass( 'jquery-confirmable-button jquery-confirmable-button-yes' )
.data( 'jquery-confirmable-button', true )
.text( options.i18n.yes );
if ( options.handler ) {
$buttonYes.on( options.events, options.handler );
}
$buttonYes = options.buttonCallback( $buttonYes, 'yes' );
// Clone it without any events and prevent default action to represent the 'No' button.
$buttonNo = $elementClone.clone( false )
.addClass( 'jquery-confirmable-button jquery-confirmable-button-no' )
.data( 'jquery-confirmable-button', true )
.text( options.i18n.no )
.on( options.events, function ( e ) {
$element.css( sideMargin, 0 );
$interface.css( 'width', 0 );
e.preventDefault();
} );
$buttonNo = options.buttonCallback( $buttonNo, 'no' );
// Prevent memory leaks
$elementClone.remove();
$interface = $( '<span>' )
.addClass( 'jquery-confirmable-interface' )
.append( $text, $buttonYes, $buttonNo );
$interface = options.wrapperCallback( $interface );
// Render offscreen to measure real width
$interface.css( positionOffscreen );
// Insert it in the correct place while we're at it
$element.after( $interface );
interfaceWidth = $interface.width();
$interface.data( 'jquery-confirmable-width', interfaceWidth );
$interface.css( positionRestore );
// Hide to animate the transition later
$interface.css( 'width', 0 );
}
// Hide element, show interface. This triggers both transitions.
// In a timeout to trigger the 'width' transition.
setTimeout( function () {
$element.css( sideMargin, -elementWidth );
$interface.css( 'width', interfaceWidth );
}, 1 );
} );
};
/**
* Default options. Overridable primarily for internationalisation handling.
* @property {Object} defaultOptions
*/
$.fn.confirmable.defaultOptions = {
events: 'click',
wrapperCallback: identity,
buttonCallback: identity,
handler: null,
i18n: {
confirm: 'Are you sure?',
yes: 'Yes',
no: 'No'
}
};
}( jQuery ) );

View file

@ -0,0 +1,13 @@
/*!
* jQuery confirmable plugin customization for MediaWiki
*
* This file serves to inject our localised messages into it.
*/
( function ( mw, $ ) {
$.fn.confirmable.defaultOptions.i18n = {
confirm: mw.message( 'confirmable-confirm', mw.user ).text(),
yes: mw.message( 'confirmable-yes' ).text(),
no: mw.message( 'confirmable-no' ).text()
};
}( mediaWiki, jQuery ) );