Merge "Type hint against LinkTarget in WatchedItemStore"
[lhc/web/wiklou.git] / resources / src / jquery / jquery.confirmable.js
1 /**
2 * jQuery confirmable plugin
3 *
4 * Released under the MIT License.
5 *
6 * @author Bartosz Dziewoński
7 *
8 * @class jQuery.plugin.confirmable
9 */
10 ( function () {
11 var identity = function ( data ) {
12 return data;
13 };
14
15 // eslint-disable-next-line valid-jsdoc
16 /**
17 * Enable inline confirmation for given clickable element (like `<a />` or `<button />`).
18 *
19 * An additional inline confirmation step being shown before the default action is carried out on
20 * click.
21 *
22 * Calling `.confirmable( { handler: function () { … } } )` will fire the handler only after the
23 * confirmation step.
24 *
25 * The element will have the `jquery-confirmable-element` class added to it when it's clicked for
26 * the first time, which has `white-space: nowrap;` and `display: inline-block;` defined in CSS.
27 * If the computed values for the element are different when you make it confirmable, you might
28 * encounter unexpected behavior.
29 *
30 * @param {Object} [options]
31 * @param {string} [options.events='click'] Events to hook to.
32 * @param {Function} [options.wrapperCallback] Callback to fire when preparing confirmable
33 * interface. Receives the interface jQuery object as the only parameter.
34 * @param {Function} [options.buttonCallback] Callback to fire when preparing confirmable buttons.
35 * It is fired separately for the 'Yes' and 'No' button. Receives the button jQuery object as
36 * the first parameter and 'yes' or 'no' as the second.
37 * @param {Function} [options.handler] Callback to fire when the action is confirmed (user clicks
38 * the 'Yes' button).
39 * @param {string} [options.delegate] Optional selector used for jQuery event delegation
40 * @param {string} [options.i18n] Text to use for interface elements.
41 * @param {string} [options.i18n.space] Word separator to place between the three text messages.
42 * @param {string} [options.i18n.confirm] Text to use for the confirmation question.
43 * @param {string} [options.i18n.yes] Text to use for the 'Yes' button.
44 * @param {string} [options.i18n.no] Text to use for the 'No' button.
45 * @param {string} [options.i18n.yesTitle] Title text to use for the 'Yes' button.
46 * @param {string} [options.i18n.noTitle] Title text to use for the 'No' button.
47 *
48 * @chainable
49 */
50 $.fn.confirmable = function ( options ) {
51 options = $.extend( true, {}, $.fn.confirmable.defaultOptions, options || {} );
52
53 if ( options.delegate === null ) {
54 return this.on( options.events, function ( e ) {
55 $.fn.confirmable.handler( e, options );
56 } );
57 }
58
59 return this.on( options.events, options.delegate, function ( e ) {
60 $.fn.confirmable.handler( e, options );
61 } );
62 };
63
64 $.fn.confirmable.handler = function ( event, options ) {
65 var $element, $text, $buttonYes, $buttonNo, $wrapper, $interface, $elementClone,
66 interfaceWidth, elementWidth, rtl, positionOffscreen, positionRestore, sideMargin;
67
68 $element = $( event.target );
69
70 if ( $element.data( 'jquery-confirmable-button' ) ) {
71 // We're running on a clone of this element that represents the 'Yes' or 'No' button.
72 // (This should never happen for the 'No' case unless calling code does bad things.)
73 return;
74 }
75
76 // Only prevent native event handling. Stopping other JavaScript event handlers
77 // is impossible because they might have already run (we have no control over the order).
78 event.preventDefault();
79
80 rtl = $element.css( 'direction' ) === 'rtl';
81 if ( rtl ) {
82 positionOffscreen = { position: 'absolute', right: '-9999px' };
83 positionRestore = { position: '', right: '' };
84 sideMargin = 'marginRight';
85 } else {
86 positionOffscreen = { position: 'absolute', left: '-9999px' };
87 positionRestore = { position: '', left: '' };
88 sideMargin = 'marginLeft';
89 }
90
91 // eslint-disable-next-line no-jquery/no-class-state
92 if ( $element.hasClass( 'jquery-confirmable-element' ) ) {
93 $wrapper = $element.closest( '.jquery-confirmable-wrapper' );
94 $interface = $wrapper.find( '.jquery-confirmable-interface' );
95 $text = $interface.find( '.jquery-confirmable-text' );
96 $buttonYes = $interface.find( '.jquery-confirmable-button-yes' );
97 $buttonNo = $interface.find( '.jquery-confirmable-button-no' );
98
99 interfaceWidth = $interface.data( 'jquery-confirmable-width' );
100 elementWidth = $element.data( 'jquery-confirmable-width' );
101 } else {
102 $elementClone = $element.clone( true );
103 $element.addClass( 'jquery-confirmable-element' );
104
105 elementWidth = $element.width();
106 $element.data( 'jquery-confirmable-width', elementWidth );
107
108 $wrapper = $( '<span>' )
109 .addClass( 'jquery-confirmable-wrapper' );
110 $element.wrap( $wrapper );
111
112 // Build the mini-dialog
113 $text = $( '<span>' )
114 .addClass( 'jquery-confirmable-text' )
115 .text( options.i18n.confirm );
116
117 // Clone original element along with event handlers to easily replicate its behavior.
118 // We could fiddle with .trigger() etc., but that is troublesome especially since
119 // Safari doesn't implement .click() on <a> links and jQuery follows suit.
120 $buttonYes = $elementClone.clone( true )
121 .addClass( 'jquery-confirmable-button jquery-confirmable-button-yes' )
122 .data( 'jquery-confirmable-button', true )
123 .text( options.i18n.yes );
124 if ( options.handler ) {
125 $buttonYes.on( options.events, options.handler );
126 }
127 if ( options.i18n.yesTitle ) {
128 $buttonYes.attr( 'title', options.i18n.yesTitle );
129 }
130 $buttonYes = options.buttonCallback( $buttonYes, 'yes' );
131
132 // Clone it without any events and prevent default action to represent the 'No' button.
133 $buttonNo = $elementClone.clone( false )
134 .addClass( 'jquery-confirmable-button jquery-confirmable-button-no' )
135 .data( 'jquery-confirmable-button', true )
136 .text( options.i18n.no )
137 .on( options.events, function ( e ) {
138 $element.css( sideMargin, 0 );
139 $interface.css( 'width', 0 );
140 e.preventDefault();
141 } );
142 if ( options.i18n.noTitle ) {
143 $buttonNo.attr( 'title', options.i18n.noTitle );
144 } else {
145 $buttonNo.removeAttr( 'title' );
146 }
147 $buttonNo = options.buttonCallback( $buttonNo, 'no' );
148
149 // Prevent memory leaks
150 $elementClone.remove();
151
152 $interface = $( '<span>' )
153 .addClass( 'jquery-confirmable-interface' )
154 .append( $text, options.i18n.space, $buttonYes, options.i18n.space, $buttonNo );
155 $interface = options.wrapperCallback( $interface );
156
157 // Render offscreen to measure real width
158 $interface.css( positionOffscreen );
159 // Insert it in the correct place while we're at it
160 $element.after( $interface );
161 interfaceWidth = $interface.width();
162 $interface.data( 'jquery-confirmable-width', interfaceWidth );
163 $interface.css( positionRestore );
164
165 // Hide to animate the transition later
166 $interface.css( 'width', 0 );
167 }
168
169 // Hide element, show interface. This triggers both transitions.
170 // In a timeout to trigger the 'width' transition.
171 setTimeout( function () {
172 $element.css( sideMargin, -elementWidth );
173 $interface.css( 'width', interfaceWidth );
174 }, 1 );
175 };
176
177 /**
178 * Default options. Overridable primarily for internationalisation handling.
179 * @property {Object} defaultOptions
180 */
181 $.fn.confirmable.defaultOptions = {
182 events: 'click',
183 wrapperCallback: identity,
184 buttonCallback: identity,
185 handler: null,
186 delegate: null,
187 i18n: {
188 space: ' ',
189 confirm: 'Are you sure?',
190 yes: 'Yes',
191 no: 'No',
192 yesTitle: undefined,
193 noTitle: undefined
194 }
195 };
196 }() );