jenkins-bot has submitted this change and it was merged. Change subject: jquery.confirmable: New inline confirmation module ......................................................................
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 --- A docs/uidesign/confirmable.html M languages/i18n/en.json M languages/i18n/he.json M languages/i18n/qqq.json M maintenance/jsduck/config.json M resources/Resources.php A resources/src/jquery/jquery.confirmable.css A resources/src/jquery/jquery.confirmable.js A resources/src/jquery/jquery.confirmable.mediawiki.js 9 files changed, 379 insertions(+), 0 deletions(-) Approvals: Legoktm: Looks good to me, approved jenkins-bot: Verified diff --git a/docs/uidesign/confirmable.html b/docs/uidesign/confirmable.html new file mode 100644 index 0000000..d035821 --- /dev/null +++ b/docs/uidesign/confirmable.html @@ -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> + diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 5fd5b6e..11c6c60 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -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}}", diff --git a/languages/i18n/he.json b/languages/i18n/he.json index a66a43a..9d0769d 100644 --- a/languages/i18n/he.json +++ b/languages/i18n/he.json @@ -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 גרסאות מחוקות}}", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 6bdc2e9..ed4e1b6 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -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?''", diff --git a/maintenance/jsduck/config.json b/maintenance/jsduck/config.json index eed76b6..e97f292 100644 --- a/maintenance/jsduck/config.json +++ b/maintenance/jsduck/config.json @@ -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", diff --git a/resources/Resources.php b/resources/Resources.php index a66faa6..b588cf2 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -191,6 +191,19 @@ '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', diff --git a/resources/src/jquery/jquery.confirmable.css b/resources/src/jquery/jquery.confirmable.css new file mode 100644 index 0000000..de69072 --- /dev/null +++ b/resources/src/jquery/jquery.confirmable.css @@ -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); +} diff --git a/resources/src/jquery/jquery.confirmable.js b/resources/src/jquery/jquery.confirmable.js new file mode 100644 index 0000000..4b80d95 --- /dev/null +++ b/resources/src/jquery/jquery.confirmable.js @@ -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 ) ); diff --git a/resources/src/jquery/jquery.confirmable.mediawiki.js b/resources/src/jquery/jquery.confirmable.mediawiki.js new file mode 100644 index 0000000..7ac04f4 --- /dev/null +++ b/resources/src/jquery/jquery.confirmable.mediawiki.js @@ -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 ) ); -- To view, visit https://gerrit.wikimedia.org/r/92315 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I2f6e0bd4f6f0a84e1a0d7193cde076738f3cdd25 Gerrit-PatchSet: 6 Gerrit-Project: mediawiki/core Gerrit-Branch: master Gerrit-Owner: Bartosz Dziewoński <matma....@gmail.com> Gerrit-Reviewer: Amire80 <amir.ahar...@mail.huji.ac.il> Gerrit-Reviewer: Anomie <bjor...@wikimedia.org> Gerrit-Reviewer: Bartosz Dziewoński <matma....@gmail.com> Gerrit-Reviewer: Catrope <roan.katt...@gmail.com> Gerrit-Reviewer: Daniel Friesen <dan...@nadir-seen-fire.com> Gerrit-Reviewer: Helder.wiki <helder.w...@gmail.com> Gerrit-Reviewer: Isarra <zhoris...@gmail.com> Gerrit-Reviewer: JGonera <jgon...@wikimedia.org> Gerrit-Reviewer: Jack Phoenix <j...@countervandalism.net> Gerrit-Reviewer: Krinkle <krinklem...@gmail.com> Gerrit-Reviewer: Legoktm <legoktm.wikipe...@gmail.com> Gerrit-Reviewer: MZMcBride <w...@mzmcbride.com> Gerrit-Reviewer: MarkTraceur <mtrac...@member.fsf.org> Gerrit-Reviewer: Mattflaschen <mflasc...@wikimedia.org> Gerrit-Reviewer: Mooeypoo <mor...@gmail.com> Gerrit-Reviewer: Ori.livneh <o...@wikimedia.org> Gerrit-Reviewer: Pginer <pgi...@wikimedia.org> Gerrit-Reviewer: PleaseStand <pleasest...@live.com> Gerrit-Reviewer: Spage <sp...@wikimedia.org> Gerrit-Reviewer: Swalling <swall...@wikimedia.org> Gerrit-Reviewer: Trevor Parscal <tpars...@wikimedia.org> Gerrit-Reviewer: Wctaiwan <wctai...@gmail.com> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits