oojs-ui-windows.js 111 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605
  1. /*!
  2. * OOUI v0.28.2
  3. * https://www.mediawiki.org/wiki/OOUI
  4. *
  5. * Copyright 2011–2018 OOUI Team and other contributors.
  6. * Released under the MIT license
  7. * http://oojs.mit-license.org
  8. *
  9. * Date: 2018-09-11T23:05:15Z
  10. */
  11. ( function ( OO ) {
  12. 'use strict';
  13. /**
  14. * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
  15. * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
  16. * of the actions.
  17. *
  18. * Both actions and action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
  19. * Please see the [OOUI documentation on MediaWiki] [1] for more information
  20. * and examples.
  21. *
  22. * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs#Action_sets
  23. *
  24. * @class
  25. * @extends OO.ui.ButtonWidget
  26. * @mixins OO.ui.mixin.PendingElement
  27. *
  28. * @constructor
  29. * @param {Object} [config] Configuration options
  30. * @cfg {string} [action] Symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
  31. * @cfg {string[]} [modes] Symbolic names of the modes (e.g., ‘edit’ or ‘read’) in which the action
  32. * should be made available. See the action set's {@link OO.ui.ActionSet#setMode setMode} method
  33. * for more information about setting modes.
  34. * @cfg {boolean} [framed=false] Render the action button with a frame
  35. */
  36. OO.ui.ActionWidget = function OoUiActionWidget( config ) {
  37. // Configuration initialization
  38. config = $.extend( { framed: false }, config );
  39. // Parent constructor
  40. OO.ui.ActionWidget.parent.call( this, config );
  41. // Mixin constructors
  42. OO.ui.mixin.PendingElement.call( this, config );
  43. // Properties
  44. this.action = config.action || '';
  45. this.modes = config.modes || [];
  46. this.width = 0;
  47. this.height = 0;
  48. // Initialization
  49. this.$element.addClass( 'oo-ui-actionWidget' );
  50. };
  51. /* Setup */
  52. OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
  53. OO.mixinClass( OO.ui.ActionWidget, OO.ui.mixin.PendingElement );
  54. /* Methods */
  55. /**
  56. * Check if the action is configured to be available in the specified `mode`.
  57. *
  58. * @param {string} mode Name of mode
  59. * @return {boolean} The action is configured with the mode
  60. */
  61. OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
  62. return this.modes.indexOf( mode ) !== -1;
  63. };
  64. /**
  65. * Get the symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
  66. *
  67. * @return {string}
  68. */
  69. OO.ui.ActionWidget.prototype.getAction = function () {
  70. return this.action;
  71. };
  72. /**
  73. * Get the symbolic name of the mode or modes for which the action is configured to be available.
  74. *
  75. * The current mode is set with the action set's {@link OO.ui.ActionSet#setMode setMode} method.
  76. * Only actions that are configured to be avaiable in the current mode will be visible. All other actions
  77. * are hidden.
  78. *
  79. * @return {string[]}
  80. */
  81. OO.ui.ActionWidget.prototype.getModes = function () {
  82. return this.modes.slice();
  83. };
  84. /* eslint-disable no-unused-vars */
  85. /**
  86. * ActionSets manage the behavior of the {@link OO.ui.ActionWidget action widgets} that comprise them.
  87. * Actions can be made available for specific contexts (modes) and circumstances
  88. * (abilities). Action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
  89. *
  90. * ActionSets contain two types of actions:
  91. *
  92. * - Special: Special actions are the first visible actions with special flags, such as 'safe' and 'primary', the default special flags. Additional special flags can be configured in subclasses with the static #specialFlags property.
  93. * - Other: Other actions include all non-special visible actions.
  94. *
  95. * See the [OOUI documentation on MediaWiki][1] for more information.
  96. *
  97. * @example
  98. * // Example: An action set used in a process dialog
  99. * function MyProcessDialog( config ) {
  100. * MyProcessDialog.parent.call( this, config );
  101. * }
  102. * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
  103. * MyProcessDialog.static.title = 'An action set in a process dialog';
  104. * MyProcessDialog.static.name = 'myProcessDialog';
  105. * // An action set that uses modes ('edit' and 'help' mode, in this example).
  106. * MyProcessDialog.static.actions = [
  107. * { action: 'continue', modes: 'edit', label: 'Continue', flags: [ 'primary', 'progressive' ] },
  108. * { action: 'help', modes: 'edit', label: 'Help' },
  109. * { modes: 'edit', label: 'Cancel', flags: 'safe' },
  110. * { action: 'back', modes: 'help', label: 'Back', flags: 'safe' }
  111. * ];
  112. *
  113. * MyProcessDialog.prototype.initialize = function () {
  114. * MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
  115. * this.panel1 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
  116. * this.panel1.$element.append( '<p>This dialog uses an action set (continue, help, cancel, back) configured with modes. This is edit mode. Click \'help\' to see help mode.</p>' );
  117. * this.panel2 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
  118. * this.panel2.$element.append( '<p>This is help mode. Only the \'back\' action widget is configured to be visible here. Click \'back\' to return to \'edit\' mode.</p>' );
  119. * this.stackLayout = new OO.ui.StackLayout( {
  120. * items: [ this.panel1, this.panel2 ]
  121. * } );
  122. * this.$body.append( this.stackLayout.$element );
  123. * };
  124. * MyProcessDialog.prototype.getSetupProcess = function ( data ) {
  125. * return MyProcessDialog.parent.prototype.getSetupProcess.call( this, data )
  126. * .next( function () {
  127. * this.actions.setMode( 'edit' );
  128. * }, this );
  129. * };
  130. * MyProcessDialog.prototype.getActionProcess = function ( action ) {
  131. * if ( action === 'help' ) {
  132. * this.actions.setMode( 'help' );
  133. * this.stackLayout.setItem( this.panel2 );
  134. * } else if ( action === 'back' ) {
  135. * this.actions.setMode( 'edit' );
  136. * this.stackLayout.setItem( this.panel1 );
  137. * } else if ( action === 'continue' ) {
  138. * var dialog = this;
  139. * return new OO.ui.Process( function () {
  140. * dialog.close();
  141. * } );
  142. * }
  143. * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
  144. * };
  145. * MyProcessDialog.prototype.getBodyHeight = function () {
  146. * return this.panel1.$element.outerHeight( true );
  147. * };
  148. * var windowManager = new OO.ui.WindowManager();
  149. * $( 'body' ).append( windowManager.$element );
  150. * var dialog = new MyProcessDialog( {
  151. * size: 'medium'
  152. * } );
  153. * windowManager.addWindows( [ dialog ] );
  154. * windowManager.openWindow( dialog );
  155. *
  156. * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs#Action_sets
  157. *
  158. * @abstract
  159. * @class
  160. * @mixins OO.EventEmitter
  161. *
  162. * @constructor
  163. * @param {Object} [config] Configuration options
  164. */
  165. OO.ui.ActionSet = function OoUiActionSet( config ) {
  166. // Configuration initialization
  167. config = config || {};
  168. // Mixin constructors
  169. OO.EventEmitter.call( this );
  170. // Properties
  171. this.list = [];
  172. this.categories = {
  173. actions: 'getAction',
  174. flags: 'getFlags',
  175. modes: 'getModes'
  176. };
  177. this.categorized = {};
  178. this.special = {};
  179. this.others = [];
  180. this.organized = false;
  181. this.changing = false;
  182. this.changed = false;
  183. };
  184. /* eslint-enable no-unused-vars */
  185. /* Setup */
  186. OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter );
  187. /* Static Properties */
  188. /**
  189. * Symbolic name of the flags used to identify special actions. Special actions are displayed in the
  190. * header of a {@link OO.ui.ProcessDialog process dialog}.
  191. * See the [OOUI documentation on MediaWiki][2] for more information and examples.
  192. *
  193. * [2]:https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs
  194. *
  195. * @abstract
  196. * @static
  197. * @inheritable
  198. * @property {string}
  199. */
  200. OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ];
  201. /* Events */
  202. /**
  203. * @event click
  204. *
  205. * A 'click' event is emitted when an action is clicked.
  206. *
  207. * @param {OO.ui.ActionWidget} action Action that was clicked
  208. */
  209. /**
  210. * @event add
  211. *
  212. * An 'add' event is emitted when actions are {@link #method-add added} to the action set.
  213. *
  214. * @param {OO.ui.ActionWidget[]} added Actions added
  215. */
  216. /**
  217. * @event remove
  218. *
  219. * A 'remove' event is emitted when actions are {@link #method-remove removed}
  220. * or {@link #clear cleared}.
  221. *
  222. * @param {OO.ui.ActionWidget[]} added Actions removed
  223. */
  224. /**
  225. * @event change
  226. *
  227. * A 'change' event is emitted when actions are {@link #method-add added}, {@link #clear cleared},
  228. * or {@link #method-remove removed} from the action set or when the {@link #setMode mode} is changed.
  229. *
  230. */
  231. /* Methods */
  232. /**
  233. * Handle action change events.
  234. *
  235. * @private
  236. * @fires change
  237. */
  238. OO.ui.ActionSet.prototype.onActionChange = function () {
  239. this.organized = false;
  240. if ( this.changing ) {
  241. this.changed = true;
  242. } else {
  243. this.emit( 'change' );
  244. }
  245. };
  246. /**
  247. * Check if an action is one of the special actions.
  248. *
  249. * @param {OO.ui.ActionWidget} action Action to check
  250. * @return {boolean} Action is special
  251. */
  252. OO.ui.ActionSet.prototype.isSpecial = function ( action ) {
  253. var flag;
  254. for ( flag in this.special ) {
  255. if ( action === this.special[ flag ] ) {
  256. return true;
  257. }
  258. }
  259. return false;
  260. };
  261. /**
  262. * Get action widgets based on the specified filter: ‘actions’, ‘flags’, ‘modes’, ‘visible’,
  263. * or ‘disabled’.
  264. *
  265. * @param {Object} [filters] Filters to use, omit to get all actions
  266. * @param {string|string[]} [filters.actions] Actions that action widgets must have
  267. * @param {string|string[]} [filters.flags] Flags that action widgets must have (e.g., 'safe')
  268. * @param {string|string[]} [filters.modes] Modes that action widgets must have
  269. * @param {boolean} [filters.visible] Action widgets must be visible
  270. * @param {boolean} [filters.disabled] Action widgets must be disabled
  271. * @return {OO.ui.ActionWidget[]} Action widgets matching all criteria
  272. */
  273. OO.ui.ActionSet.prototype.get = function ( filters ) {
  274. var i, len, list, category, actions, index, match, matches;
  275. if ( filters ) {
  276. this.organize();
  277. // Collect category candidates
  278. matches = [];
  279. for ( category in this.categorized ) {
  280. list = filters[ category ];
  281. if ( list ) {
  282. if ( !Array.isArray( list ) ) {
  283. list = [ list ];
  284. }
  285. for ( i = 0, len = list.length; i < len; i++ ) {
  286. actions = this.categorized[ category ][ list[ i ] ];
  287. if ( Array.isArray( actions ) ) {
  288. matches.push.apply( matches, actions );
  289. }
  290. }
  291. }
  292. }
  293. // Remove by boolean filters
  294. for ( i = 0, len = matches.length; i < len; i++ ) {
  295. match = matches[ i ];
  296. if (
  297. ( filters.visible !== undefined && match.isVisible() !== filters.visible ) ||
  298. ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled )
  299. ) {
  300. matches.splice( i, 1 );
  301. len--;
  302. i--;
  303. }
  304. }
  305. // Remove duplicates
  306. for ( i = 0, len = matches.length; i < len; i++ ) {
  307. match = matches[ i ];
  308. index = matches.lastIndexOf( match );
  309. while ( index !== i ) {
  310. matches.splice( index, 1 );
  311. len--;
  312. index = matches.lastIndexOf( match );
  313. }
  314. }
  315. return matches;
  316. }
  317. return this.list.slice();
  318. };
  319. /**
  320. * Get 'special' actions.
  321. *
  322. * Special actions are the first visible action widgets with special flags, such as 'safe' and 'primary'.
  323. * Special flags can be configured in subclasses by changing the static #specialFlags property.
  324. *
  325. * @return {OO.ui.ActionWidget[]|null} 'Special' action widgets.
  326. */
  327. OO.ui.ActionSet.prototype.getSpecial = function () {
  328. this.organize();
  329. return $.extend( {}, this.special );
  330. };
  331. /**
  332. * Get 'other' actions.
  333. *
  334. * Other actions include all non-special visible action widgets.
  335. *
  336. * @return {OO.ui.ActionWidget[]} 'Other' action widgets
  337. */
  338. OO.ui.ActionSet.prototype.getOthers = function () {
  339. this.organize();
  340. return this.others.slice();
  341. };
  342. /**
  343. * Set the mode (e.g., ‘edit’ or ‘view’). Only {@link OO.ui.ActionWidget#modes actions} configured
  344. * to be available in the specified mode will be made visible. All other actions will be hidden.
  345. *
  346. * @param {string} mode The mode. Only actions configured to be available in the specified
  347. * mode will be made visible.
  348. * @chainable
  349. * @fires toggle
  350. * @fires change
  351. */
  352. OO.ui.ActionSet.prototype.setMode = function ( mode ) {
  353. var i, len, action;
  354. this.changing = true;
  355. for ( i = 0, len = this.list.length; i < len; i++ ) {
  356. action = this.list[ i ];
  357. action.toggle( action.hasMode( mode ) );
  358. }
  359. this.organized = false;
  360. this.changing = false;
  361. this.emit( 'change' );
  362. return this;
  363. };
  364. /**
  365. * Set the abilities of the specified actions.
  366. *
  367. * Action widgets that are configured with the specified actions will be enabled
  368. * or disabled based on the boolean values specified in the `actions`
  369. * parameter.
  370. *
  371. * @param {Object.<string,boolean>} actions A list keyed by action name with boolean
  372. * values that indicate whether or not the action should be enabled.
  373. * @chainable
  374. */
  375. OO.ui.ActionSet.prototype.setAbilities = function ( actions ) {
  376. var i, len, action, item;
  377. for ( i = 0, len = this.list.length; i < len; i++ ) {
  378. item = this.list[ i ];
  379. action = item.getAction();
  380. if ( actions[ action ] !== undefined ) {
  381. item.setDisabled( !actions[ action ] );
  382. }
  383. }
  384. return this;
  385. };
  386. /**
  387. * Executes a function once per action.
  388. *
  389. * When making changes to multiple actions, use this method instead of iterating over the actions
  390. * manually to defer emitting a #change event until after all actions have been changed.
  391. *
  392. * @param {Object|null} filter Filters to use to determine which actions to iterate over; see #get
  393. * @param {Function} callback Callback to run for each action; callback is invoked with three
  394. * arguments: the action, the action's index, the list of actions being iterated over
  395. * @chainable
  396. */
  397. OO.ui.ActionSet.prototype.forEach = function ( filter, callback ) {
  398. this.changed = false;
  399. this.changing = true;
  400. this.get( filter ).forEach( callback );
  401. this.changing = false;
  402. if ( this.changed ) {
  403. this.emit( 'change' );
  404. }
  405. return this;
  406. };
  407. /**
  408. * Add action widgets to the action set.
  409. *
  410. * @param {OO.ui.ActionWidget[]} actions Action widgets to add
  411. * @chainable
  412. * @fires add
  413. * @fires change
  414. */
  415. OO.ui.ActionSet.prototype.add = function ( actions ) {
  416. var i, len, action;
  417. this.changing = true;
  418. for ( i = 0, len = actions.length; i < len; i++ ) {
  419. action = actions[ i ];
  420. action.connect( this, {
  421. click: [ 'emit', 'click', action ],
  422. toggle: [ 'onActionChange' ]
  423. } );
  424. this.list.push( action );
  425. }
  426. this.organized = false;
  427. this.emit( 'add', actions );
  428. this.changing = false;
  429. this.emit( 'change' );
  430. return this;
  431. };
  432. /**
  433. * Remove action widgets from the set.
  434. *
  435. * To remove all actions, you may wish to use the #clear method instead.
  436. *
  437. * @param {OO.ui.ActionWidget[]} actions Action widgets to remove
  438. * @chainable
  439. * @fires remove
  440. * @fires change
  441. */
  442. OO.ui.ActionSet.prototype.remove = function ( actions ) {
  443. var i, len, index, action;
  444. this.changing = true;
  445. for ( i = 0, len = actions.length; i < len; i++ ) {
  446. action = actions[ i ];
  447. index = this.list.indexOf( action );
  448. if ( index !== -1 ) {
  449. action.disconnect( this );
  450. this.list.splice( index, 1 );
  451. }
  452. }
  453. this.organized = false;
  454. this.emit( 'remove', actions );
  455. this.changing = false;
  456. this.emit( 'change' );
  457. return this;
  458. };
  459. /**
  460. * Remove all action widets from the set.
  461. *
  462. * To remove only specified actions, use the {@link #method-remove remove} method instead.
  463. *
  464. * @chainable
  465. * @fires remove
  466. * @fires change
  467. */
  468. OO.ui.ActionSet.prototype.clear = function () {
  469. var i, len, action,
  470. removed = this.list.slice();
  471. this.changing = true;
  472. for ( i = 0, len = this.list.length; i < len; i++ ) {
  473. action = this.list[ i ];
  474. action.disconnect( this );
  475. }
  476. this.list = [];
  477. this.organized = false;
  478. this.emit( 'remove', removed );
  479. this.changing = false;
  480. this.emit( 'change' );
  481. return this;
  482. };
  483. /**
  484. * Organize actions.
  485. *
  486. * This is called whenever organized information is requested. It will only reorganize the actions
  487. * if something has changed since the last time it ran.
  488. *
  489. * @private
  490. * @chainable
  491. */
  492. OO.ui.ActionSet.prototype.organize = function () {
  493. var i, iLen, j, jLen, flag, action, category, list, item, special,
  494. specialFlags = this.constructor.static.specialFlags;
  495. if ( !this.organized ) {
  496. this.categorized = {};
  497. this.special = {};
  498. this.others = [];
  499. for ( i = 0, iLen = this.list.length; i < iLen; i++ ) {
  500. action = this.list[ i ];
  501. if ( action.isVisible() ) {
  502. // Populate categories
  503. for ( category in this.categories ) {
  504. if ( !this.categorized[ category ] ) {
  505. this.categorized[ category ] = {};
  506. }
  507. list = action[ this.categories[ category ] ]();
  508. if ( !Array.isArray( list ) ) {
  509. list = [ list ];
  510. }
  511. for ( j = 0, jLen = list.length; j < jLen; j++ ) {
  512. item = list[ j ];
  513. if ( !this.categorized[ category ][ item ] ) {
  514. this.categorized[ category ][ item ] = [];
  515. }
  516. this.categorized[ category ][ item ].push( action );
  517. }
  518. }
  519. // Populate special/others
  520. special = false;
  521. for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) {
  522. flag = specialFlags[ j ];
  523. if ( !this.special[ flag ] && action.hasFlag( flag ) ) {
  524. this.special[ flag ] = action;
  525. special = true;
  526. break;
  527. }
  528. }
  529. if ( !special ) {
  530. this.others.push( action );
  531. }
  532. }
  533. }
  534. this.organized = true;
  535. }
  536. return this;
  537. };
  538. /**
  539. * Errors contain a required message (either a string or jQuery selection) that is used to describe what went wrong
  540. * in a {@link OO.ui.Process process}. The error's #recoverable and #warning configurations are used to customize the
  541. * appearance and functionality of the error interface.
  542. *
  543. * The basic error interface contains a formatted error message as well as two buttons: 'Dismiss' and 'Try again' (i.e., the error
  544. * is 'recoverable' by default). If the error is not recoverable, the 'Try again' button will not be rendered and the widget
  545. * that initiated the failed process will be disabled.
  546. *
  547. * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button, which will try the
  548. * process again.
  549. *
  550. * For an example of error interfaces, please see the [OOUI documentation on MediaWiki][1].
  551. *
  552. * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs#Processes_and_errors
  553. *
  554. * @class
  555. *
  556. * @constructor
  557. * @param {string|jQuery} message Description of error
  558. * @param {Object} [config] Configuration options
  559. * @cfg {boolean} [recoverable=true] Error is recoverable.
  560. * By default, errors are recoverable, and users can try the process again.
  561. * @cfg {boolean} [warning=false] Error is a warning.
  562. * If the error is a warning, the error interface will include a
  563. * 'Dismiss' and a 'Continue' button. It is the responsibility of the developer to ensure that the warning
  564. * is not triggered a second time if the user chooses to continue.
  565. */
  566. OO.ui.Error = function OoUiError( message, config ) {
  567. // Allow passing positional parameters inside the config object
  568. if ( OO.isPlainObject( message ) && config === undefined ) {
  569. config = message;
  570. message = config.message;
  571. }
  572. // Configuration initialization
  573. config = config || {};
  574. // Properties
  575. this.message = message instanceof jQuery ? message : String( message );
  576. this.recoverable = config.recoverable === undefined || !!config.recoverable;
  577. this.warning = !!config.warning;
  578. };
  579. /* Setup */
  580. OO.initClass( OO.ui.Error );
  581. /* Methods */
  582. /**
  583. * Check if the error is recoverable.
  584. *
  585. * If the error is recoverable, users are able to try the process again.
  586. *
  587. * @return {boolean} Error is recoverable
  588. */
  589. OO.ui.Error.prototype.isRecoverable = function () {
  590. return this.recoverable;
  591. };
  592. /**
  593. * Check if the error is a warning.
  594. *
  595. * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button.
  596. *
  597. * @return {boolean} Error is warning
  598. */
  599. OO.ui.Error.prototype.isWarning = function () {
  600. return this.warning;
  601. };
  602. /**
  603. * Get error message as DOM nodes.
  604. *
  605. * @return {jQuery} Error message in DOM nodes
  606. */
  607. OO.ui.Error.prototype.getMessage = function () {
  608. return this.message instanceof jQuery ?
  609. this.message.clone() :
  610. $( '<div>' ).text( this.message ).contents();
  611. };
  612. /**
  613. * Get the error message text.
  614. *
  615. * @return {string} Error message
  616. */
  617. OO.ui.Error.prototype.getMessageText = function () {
  618. return this.message instanceof jQuery ? this.message.text() : this.message;
  619. };
  620. /**
  621. * A Process is a list of steps that are called in sequence. The step can be a number, a jQuery promise,
  622. * or a function:
  623. *
  624. * - **number**: the process will wait for the specified number of milliseconds before proceeding.
  625. * - **promise**: the process will continue to the next step when the promise is successfully resolved
  626. * or stop if the promise is rejected.
  627. * - **function**: the process will execute the function. The process will stop if the function returns
  628. * either a boolean `false` or a promise that is rejected; if the function returns a number, the process
  629. * will wait for that number of milliseconds before proceeding.
  630. *
  631. * If the process fails, an {@link OO.ui.Error error} is generated. Depending on how the error is
  632. * configured, users can dismiss the error and try the process again, or not. If a process is stopped,
  633. * its remaining steps will not be performed.
  634. *
  635. * @class
  636. *
  637. * @constructor
  638. * @param {number|jQuery.Promise|Function} step Number of miliseconds to wait before proceeding, promise
  639. * that must be resolved before proceeding, or a function to execute. See #createStep for more information. see #createStep for more information
  640. * @param {Object} [context=null] Execution context of the function. The context is ignored if the step is
  641. * a number or promise.
  642. */
  643. OO.ui.Process = function ( step, context ) {
  644. // Properties
  645. this.steps = [];
  646. // Initialization
  647. if ( step !== undefined ) {
  648. this.next( step, context );
  649. }
  650. };
  651. /* Setup */
  652. OO.initClass( OO.ui.Process );
  653. /* Methods */
  654. /**
  655. * Start the process.
  656. *
  657. * @return {jQuery.Promise} Promise that is resolved when all steps have successfully completed.
  658. * If any of the steps return a promise that is rejected or a boolean false, this promise is rejected
  659. * and any remaining steps are not performed.
  660. */
  661. OO.ui.Process.prototype.execute = function () {
  662. var i, len, promise;
  663. /**
  664. * Continue execution.
  665. *
  666. * @ignore
  667. * @param {Array} step A function and the context it should be called in
  668. * @return {Function} Function that continues the process
  669. */
  670. function proceed( step ) {
  671. return function () {
  672. // Execute step in the correct context
  673. var deferred,
  674. result = step.callback.call( step.context );
  675. if ( result === false ) {
  676. // Use rejected promise for boolean false results
  677. return $.Deferred().reject( [] ).promise();
  678. }
  679. if ( typeof result === 'number' ) {
  680. if ( result < 0 ) {
  681. throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
  682. }
  683. // Use a delayed promise for numbers, expecting them to be in milliseconds
  684. deferred = $.Deferred();
  685. setTimeout( deferred.resolve, result );
  686. return deferred.promise();
  687. }
  688. if ( result instanceof OO.ui.Error ) {
  689. // Use rejected promise for error
  690. return $.Deferred().reject( [ result ] ).promise();
  691. }
  692. if ( Array.isArray( result ) && result.length && result[ 0 ] instanceof OO.ui.Error ) {
  693. // Use rejected promise for list of errors
  694. return $.Deferred().reject( result ).promise();
  695. }
  696. // Duck-type the object to see if it can produce a promise
  697. if ( result && $.isFunction( result.promise ) ) {
  698. // Use a promise generated from the result
  699. return result.promise();
  700. }
  701. // Use resolved promise for other results
  702. return $.Deferred().resolve().promise();
  703. };
  704. }
  705. if ( this.steps.length ) {
  706. // Generate a chain reaction of promises
  707. promise = proceed( this.steps[ 0 ] )();
  708. for ( i = 1, len = this.steps.length; i < len; i++ ) {
  709. promise = promise.then( proceed( this.steps[ i ] ) );
  710. }
  711. } else {
  712. promise = $.Deferred().resolve().promise();
  713. }
  714. return promise;
  715. };
  716. /**
  717. * Create a process step.
  718. *
  719. * @private
  720. * @param {number|jQuery.Promise|Function} step
  721. *
  722. * - Number of milliseconds to wait before proceeding
  723. * - Promise that must be resolved before proceeding
  724. * - Function to execute
  725. * - If the function returns a boolean false the process will stop
  726. * - If the function returns a promise, the process will continue to the next
  727. * step when the promise is resolved or stop if the promise is rejected
  728. * - If the function returns a number, the process will wait for that number of
  729. * milliseconds before proceeding
  730. * @param {Object} [context=null] Execution context of the function. The context is
  731. * ignored if the step is a number or promise.
  732. * @return {Object} Step object, with `callback` and `context` properties
  733. */
  734. OO.ui.Process.prototype.createStep = function ( step, context ) {
  735. if ( typeof step === 'number' || $.isFunction( step.promise ) ) {
  736. return {
  737. callback: function () {
  738. return step;
  739. },
  740. context: null
  741. };
  742. }
  743. if ( $.isFunction( step ) ) {
  744. return {
  745. callback: step,
  746. context: context
  747. };
  748. }
  749. throw new Error( 'Cannot create process step: number, promise or function expected' );
  750. };
  751. /**
  752. * Add step to the beginning of the process.
  753. *
  754. * @inheritdoc #createStep
  755. * @return {OO.ui.Process} this
  756. * @chainable
  757. */
  758. OO.ui.Process.prototype.first = function ( step, context ) {
  759. this.steps.unshift( this.createStep( step, context ) );
  760. return this;
  761. };
  762. /**
  763. * Add step to the end of the process.
  764. *
  765. * @inheritdoc #createStep
  766. * @return {OO.ui.Process} this
  767. * @chainable
  768. */
  769. OO.ui.Process.prototype.next = function ( step, context ) {
  770. this.steps.push( this.createStep( step, context ) );
  771. return this;
  772. };
  773. /**
  774. * A window instance represents the life cycle for one single opening of a window
  775. * until its closing.
  776. *
  777. * While OO.ui.WindowManager will reuse OO.ui.Window objects, each time a window is
  778. * opened, a new lifecycle starts.
  779. *
  780. * For more information, please see the [OOUI documentation on MediaWiki] [1].
  781. *
  782. * [1]: https://www.mediawiki.org/wiki/OOUI/Windows
  783. *
  784. * @class
  785. *
  786. * @constructor
  787. */
  788. OO.ui.WindowInstance = function OOuiWindowInstance() {
  789. var deferreds = {
  790. opening: $.Deferred(),
  791. opened: $.Deferred(),
  792. closing: $.Deferred(),
  793. closed: $.Deferred()
  794. };
  795. /**
  796. * @private
  797. * @property {Object}
  798. */
  799. this.deferreds = deferreds;
  800. // Set these up as chained promises so that rejecting of
  801. // an earlier stage automatically rejects the subsequent
  802. // would-be stages as well.
  803. /**
  804. * @property {jQuery.Promise}
  805. */
  806. this.opening = deferreds.opening.promise();
  807. /**
  808. * @property {jQuery.Promise}
  809. */
  810. this.opened = this.opening.then( function () {
  811. return deferreds.opened;
  812. } );
  813. /**
  814. * @property {jQuery.Promise}
  815. */
  816. this.closing = this.opened.then( function () {
  817. return deferreds.closing;
  818. } );
  819. /**
  820. * @property {jQuery.Promise}
  821. */
  822. this.closed = this.closing.then( function () {
  823. return deferreds.closed;
  824. } );
  825. };
  826. /* Setup */
  827. OO.initClass( OO.ui.WindowInstance );
  828. /**
  829. * Check if window is opening.
  830. *
  831. * @return {boolean} Window is opening
  832. */
  833. OO.ui.WindowInstance.prototype.isOpening = function () {
  834. return this.deferreds.opened.state() === 'pending';
  835. };
  836. /**
  837. * Check if window is opened.
  838. *
  839. * @return {boolean} Window is opened
  840. */
  841. OO.ui.WindowInstance.prototype.isOpened = function () {
  842. return this.deferreds.opened.state() === 'resolved' &&
  843. this.deferreds.closing.state() === 'pending';
  844. };
  845. /**
  846. * Check if window is closing.
  847. *
  848. * @return {boolean} Window is closing
  849. */
  850. OO.ui.WindowInstance.prototype.isClosing = function () {
  851. return this.deferreds.closing.state() === 'resolved' &&
  852. this.deferreds.closed.state() === 'pending';
  853. };
  854. /**
  855. * Check if window is closed.
  856. *
  857. * @return {boolean} Window is closed
  858. */
  859. OO.ui.WindowInstance.prototype.isClosed = function () {
  860. return this.deferreds.closed.state() === 'resolved';
  861. };
  862. /**
  863. * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation.
  864. * Managed windows are mutually exclusive. If a new window is opened while a current window is opening
  865. * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows
  866. * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
  867. * pertinent data and reused.
  868. *
  869. * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
  870. * `opened`, and `closing`, which represent the primary stages of the cycle:
  871. *
  872. * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
  873. * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
  874. *
  875. * - an `opening` event is emitted with an `opening` promise
  876. * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before the
  877. * window’s {@link OO.ui.Window#method-setup setup} method is called which executes OO.ui.Window#getSetupProcess.
  878. * - a `setup` progress notification is emitted from the `opening` promise
  879. * - the #getReadyDelay method is called the returned value is used to time a pause in execution before the
  880. * window’s {@link OO.ui.Window#method-ready ready} method is called which executes OO.ui.Window#getReadyProcess.
  881. * - a `ready` progress notification is emitted from the `opening` promise
  882. * - the `opening` promise is resolved with an `opened` promise
  883. *
  884. * **Opened**: the window is now open.
  885. *
  886. * **Closing**: the closing stage begins when the window manager's #closeWindow or the
  887. * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
  888. * to close the window.
  889. *
  890. * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
  891. * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before
  892. * the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the
  893. * window and its result executed
  894. * - a `hold` progress notification is emitted from the `closing` promise
  895. * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before
  896. * the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the
  897. * window and its result executed
  898. * - a `teardown` progress notification is emitted from the `closing` promise
  899. * - the `closing` promise is resolved. The window is now closed
  900. *
  901. * See the [OOUI documentation on MediaWiki][1] for more information.
  902. *
  903. * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Window_managers
  904. *
  905. * @class
  906. * @extends OO.ui.Element
  907. * @mixins OO.EventEmitter
  908. *
  909. * @constructor
  910. * @param {Object} [config] Configuration options
  911. * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
  912. * Note that window classes that are instantiated with a factory must have
  913. * a {@link OO.ui.Dialog#static-name static name} property that specifies a symbolic name.
  914. * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
  915. */
  916. OO.ui.WindowManager = function OoUiWindowManager( config ) {
  917. // Configuration initialization
  918. config = config || {};
  919. // Parent constructor
  920. OO.ui.WindowManager.parent.call( this, config );
  921. // Mixin constructors
  922. OO.EventEmitter.call( this );
  923. // Properties
  924. this.factory = config.factory;
  925. this.modal = config.modal === undefined || !!config.modal;
  926. this.windows = {};
  927. // Deprecated placeholder promise given to compatOpening in openWindow()
  928. // that is resolved in closeWindow().
  929. this.compatOpened = null;
  930. this.preparingToOpen = null;
  931. this.preparingToClose = null;
  932. this.currentWindow = null;
  933. this.globalEvents = false;
  934. this.$returnFocusTo = null;
  935. this.$ariaHidden = null;
  936. this.onWindowResizeTimeout = null;
  937. this.onWindowResizeHandler = this.onWindowResize.bind( this );
  938. this.afterWindowResizeHandler = this.afterWindowResize.bind( this );
  939. // Initialization
  940. this.$element
  941. .addClass( 'oo-ui-windowManager' )
  942. .toggleClass( 'oo-ui-windowManager-modal', this.modal );
  943. if ( this.modal ) {
  944. this.$element.attr( 'aria-hidden', true );
  945. }
  946. };
  947. /* Setup */
  948. OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
  949. OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
  950. /* Events */
  951. /**
  952. * An 'opening' event is emitted when the window begins to be opened.
  953. *
  954. * @event opening
  955. * @param {OO.ui.Window} win Window that's being opened
  956. * @param {jQuery.Promise} opened A promise resolved with a value when the window is opened successfully.
  957. * This promise also emits `setup` and `ready` notifications. When this promise is resolved, the first
  958. * argument of the value is an 'closed' promise, the second argument is the opening data.
  959. * @param {Object} data Window opening data
  960. */
  961. /**
  962. * A 'closing' event is emitted when the window begins to be closed.
  963. *
  964. * @event closing
  965. * @param {OO.ui.Window} win Window that's being closed
  966. * @param {jQuery.Promise} closed A promise resolved with a value when the window is closed successfully.
  967. * This promise also emits `hold` and `teardown` notifications. When this promise is resolved, the first
  968. * argument of its value is the closing data.
  969. * @param {Object} data Window closing data
  970. */
  971. /**
  972. * A 'resize' event is emitted when a window is resized.
  973. *
  974. * @event resize
  975. * @param {OO.ui.Window} win Window that was resized
  976. */
  977. /* Static Properties */
  978. /**
  979. * Map of the symbolic name of each window size and its CSS properties.
  980. *
  981. * @static
  982. * @inheritable
  983. * @property {Object}
  984. */
  985. OO.ui.WindowManager.static.sizes = {
  986. small: {
  987. width: 300
  988. },
  989. medium: {
  990. width: 500
  991. },
  992. large: {
  993. width: 700
  994. },
  995. larger: {
  996. width: 900
  997. },
  998. full: {
  999. // These can be non-numeric because they are never used in calculations
  1000. width: '100%',
  1001. height: '100%'
  1002. }
  1003. };
  1004. /**
  1005. * Symbolic name of the default window size.
  1006. *
  1007. * The default size is used if the window's requested size is not recognized.
  1008. *
  1009. * @static
  1010. * @inheritable
  1011. * @property {string}
  1012. */
  1013. OO.ui.WindowManager.static.defaultSize = 'medium';
  1014. /* Methods */
  1015. /**
  1016. * Handle window resize events.
  1017. *
  1018. * @private
  1019. * @param {jQuery.Event} e Window resize event
  1020. */
  1021. OO.ui.WindowManager.prototype.onWindowResize = function () {
  1022. clearTimeout( this.onWindowResizeTimeout );
  1023. this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
  1024. };
  1025. /**
  1026. * Handle window resize events.
  1027. *
  1028. * @private
  1029. * @param {jQuery.Event} e Window resize event
  1030. */
  1031. OO.ui.WindowManager.prototype.afterWindowResize = function () {
  1032. var currentFocusedElement = document.activeElement;
  1033. if ( this.currentWindow ) {
  1034. this.updateWindowSize( this.currentWindow );
  1035. // Restore focus to the original element if it has changed.
  1036. // When a layout change is made on resize inputs lose focus
  1037. // on Android (Chrome and Firefox). See T162127.
  1038. if ( currentFocusedElement !== document.activeElement ) {
  1039. currentFocusedElement.focus();
  1040. }
  1041. }
  1042. };
  1043. /**
  1044. * Check if window is opening.
  1045. *
  1046. * @param {OO.ui.Window} win Window to check
  1047. * @return {boolean} Window is opening
  1048. */
  1049. OO.ui.WindowManager.prototype.isOpening = function ( win ) {
  1050. return win === this.currentWindow && !!this.lifecycle &&
  1051. this.lifecycle.isOpening();
  1052. };
  1053. /**
  1054. * Check if window is closing.
  1055. *
  1056. * @param {OO.ui.Window} win Window to check
  1057. * @return {boolean} Window is closing
  1058. */
  1059. OO.ui.WindowManager.prototype.isClosing = function ( win ) {
  1060. return win === this.currentWindow && !!this.lifecycle &&
  1061. this.lifecycle.isClosing();
  1062. };
  1063. /**
  1064. * Check if window is opened.
  1065. *
  1066. * @param {OO.ui.Window} win Window to check
  1067. * @return {boolean} Window is opened
  1068. */
  1069. OO.ui.WindowManager.prototype.isOpened = function ( win ) {
  1070. return win === this.currentWindow && !!this.lifecycle &&
  1071. this.lifecycle.isOpened();
  1072. };
  1073. /**
  1074. * Check if a window is being managed.
  1075. *
  1076. * @param {OO.ui.Window} win Window to check
  1077. * @return {boolean} Window is being managed
  1078. */
  1079. OO.ui.WindowManager.prototype.hasWindow = function ( win ) {
  1080. var name;
  1081. for ( name in this.windows ) {
  1082. if ( this.windows[ name ] === win ) {
  1083. return true;
  1084. }
  1085. }
  1086. return false;
  1087. };
  1088. /**
  1089. * Get the number of milliseconds to wait after opening begins before executing the ‘setup’ process.
  1090. *
  1091. * @param {OO.ui.Window} win Window being opened
  1092. * @param {Object} [data] Window opening data
  1093. * @return {number} Milliseconds to wait
  1094. */
  1095. OO.ui.WindowManager.prototype.getSetupDelay = function () {
  1096. return 0;
  1097. };
  1098. /**
  1099. * Get the number of milliseconds to wait after setup has finished before executing the ‘ready’ process.
  1100. *
  1101. * @param {OO.ui.Window} win Window being opened
  1102. * @param {Object} [data] Window opening data
  1103. * @return {number} Milliseconds to wait
  1104. */
  1105. OO.ui.WindowManager.prototype.getReadyDelay = function () {
  1106. return this.modal ? OO.ui.theme.getDialogTransitionDuration() : 0;
  1107. };
  1108. /**
  1109. * Get the number of milliseconds to wait after closing has begun before executing the 'hold' process.
  1110. *
  1111. * @param {OO.ui.Window} win Window being closed
  1112. * @param {Object} [data] Window closing data
  1113. * @return {number} Milliseconds to wait
  1114. */
  1115. OO.ui.WindowManager.prototype.getHoldDelay = function () {
  1116. return 0;
  1117. };
  1118. /**
  1119. * Get the number of milliseconds to wait after the ‘hold’ process has finished before
  1120. * executing the ‘teardown’ process.
  1121. *
  1122. * @param {OO.ui.Window} win Window being closed
  1123. * @param {Object} [data] Window closing data
  1124. * @return {number} Milliseconds to wait
  1125. */
  1126. OO.ui.WindowManager.prototype.getTeardownDelay = function () {
  1127. return this.modal ? OO.ui.theme.getDialogTransitionDuration() : 0;
  1128. };
  1129. /**
  1130. * Get a window by its symbolic name.
  1131. *
  1132. * If the window is not yet instantiated and its symbolic name is recognized by a factory, it will be
  1133. * instantiated and added to the window manager automatically. Please see the [OOUI documentation on MediaWiki][3]
  1134. * for more information about using factories.
  1135. * [3]: https://www.mediawiki.org/wiki/OOUI/Windows/Window_managers
  1136. *
  1137. * @param {string} name Symbolic name of the window
  1138. * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
  1139. * @throws {Error} An error is thrown if the symbolic name is not recognized by the factory.
  1140. * @throws {Error} An error is thrown if the named window is not recognized as a managed window.
  1141. */
  1142. OO.ui.WindowManager.prototype.getWindow = function ( name ) {
  1143. var deferred = $.Deferred(),
  1144. win = this.windows[ name ];
  1145. if ( !( win instanceof OO.ui.Window ) ) {
  1146. if ( this.factory ) {
  1147. if ( !this.factory.lookup( name ) ) {
  1148. deferred.reject( new OO.ui.Error(
  1149. 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
  1150. ) );
  1151. } else {
  1152. win = this.factory.create( name );
  1153. this.addWindows( [ win ] );
  1154. deferred.resolve( win );
  1155. }
  1156. } else {
  1157. deferred.reject( new OO.ui.Error(
  1158. 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
  1159. ) );
  1160. }
  1161. } else {
  1162. deferred.resolve( win );
  1163. }
  1164. return deferred.promise();
  1165. };
  1166. /**
  1167. * Get current window.
  1168. *
  1169. * @return {OO.ui.Window|null} Currently opening/opened/closing window
  1170. */
  1171. OO.ui.WindowManager.prototype.getCurrentWindow = function () {
  1172. return this.currentWindow;
  1173. };
  1174. /**
  1175. * Open a window.
  1176. *
  1177. * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
  1178. * @param {Object} [data] Window opening data
  1179. * @param {jQuery|null} [data.$returnFocusTo] Element to which the window will return focus when closed.
  1180. * Defaults the current activeElement. If set to null, focus isn't changed on close.
  1181. * @return {OO.ui.WindowInstance} A lifecycle object representing this particular
  1182. * opening of the window. For backwards-compatibility, then object is also a Thenable that is resolved
  1183. * when the window is done opening, with nested promise for when closing starts. This behaviour
  1184. * is deprecated and is not compatible with jQuery 3. See T163510.
  1185. * @fires opening
  1186. */
  1187. OO.ui.WindowManager.prototype.openWindow = function ( win, data, lifecycle, compatOpening ) {
  1188. var error,
  1189. manager = this;
  1190. data = data || {};
  1191. // Internal parameter 'lifecycle' allows this method to always return
  1192. // a lifecycle even if the window still needs to be created
  1193. // asynchronously when 'win' is a string.
  1194. lifecycle = lifecycle || new OO.ui.WindowInstance();
  1195. compatOpening = compatOpening || $.Deferred();
  1196. // Turn lifecycle into a Thenable for backwards-compatibility with
  1197. // the deprecated nested-promise behaviour, see T163510.
  1198. [ 'state', 'always', 'catch', 'pipe', 'then', 'promise', 'progress', 'done', 'fail' ]
  1199. .forEach( function ( method ) {
  1200. lifecycle[ method ] = function () {
  1201. OO.ui.warnDeprecation(
  1202. 'Using the return value of openWindow as a promise is deprecated. ' +
  1203. 'Use .openWindow( ... ).opening.' + method + '( ... ) instead.'
  1204. );
  1205. return compatOpening[ method ].apply( this, arguments );
  1206. };
  1207. } );
  1208. // Argument handling
  1209. if ( typeof win === 'string' ) {
  1210. this.getWindow( win ).then(
  1211. function ( win ) {
  1212. manager.openWindow( win, data, lifecycle, compatOpening );
  1213. },
  1214. function ( err ) {
  1215. lifecycle.deferreds.opening.reject( err );
  1216. }
  1217. );
  1218. return lifecycle;
  1219. }
  1220. // Error handling
  1221. if ( !this.hasWindow( win ) ) {
  1222. error = 'Cannot open window: window is not attached to manager';
  1223. } else if ( this.lifecycle && this.lifecycle.isOpened() ) {
  1224. error = 'Cannot open window: another window is open';
  1225. } else if ( this.preparingToOpen || ( this.lifecycle && this.lifecycle.isOpening() ) ) {
  1226. error = 'Cannot open window: another window is opening';
  1227. }
  1228. if ( error ) {
  1229. compatOpening.reject( new OO.ui.Error( error ) );
  1230. lifecycle.deferreds.opening.reject( new OO.ui.Error( error ) );
  1231. return lifecycle;
  1232. }
  1233. // If a window is currently closing, wait for it to complete
  1234. this.preparingToOpen = $.when( this.lifecycle && this.lifecycle.closed );
  1235. // Ensure handlers get called after preparingToOpen is set
  1236. this.preparingToOpen.done( function () {
  1237. if ( manager.modal ) {
  1238. manager.toggleGlobalEvents( true );
  1239. manager.toggleAriaIsolation( true );
  1240. }
  1241. manager.$returnFocusTo = data.$returnFocusTo !== undefined ? data.$returnFocusTo : $( document.activeElement );
  1242. manager.currentWindow = win;
  1243. manager.lifecycle = lifecycle;
  1244. manager.preparingToOpen = null;
  1245. manager.emit( 'opening', win, compatOpening, data );
  1246. lifecycle.deferreds.opening.resolve( data );
  1247. setTimeout( function () {
  1248. manager.compatOpened = $.Deferred();
  1249. win.setup( data ).then( function () {
  1250. compatOpening.notify( { state: 'setup' } );
  1251. setTimeout( function () {
  1252. win.ready( data ).then( function () {
  1253. compatOpening.notify( { state: 'ready' } );
  1254. lifecycle.deferreds.opened.resolve( data );
  1255. compatOpening.resolve( manager.compatOpened.promise(), data );
  1256. }, function () {
  1257. lifecycle.deferreds.opened.reject();
  1258. compatOpening.reject();
  1259. manager.closeWindow( win );
  1260. } );
  1261. }, manager.getReadyDelay() );
  1262. }, function () {
  1263. lifecycle.deferreds.opened.reject();
  1264. compatOpening.reject();
  1265. manager.closeWindow( win );
  1266. } );
  1267. }, manager.getSetupDelay() );
  1268. } );
  1269. return lifecycle;
  1270. };
  1271. /**
  1272. * Close a window.
  1273. *
  1274. * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
  1275. * @param {Object} [data] Window closing data
  1276. * @return {OO.ui.WindowInstance} A lifecycle object representing this particular
  1277. * opening of the window. For backwards-compatibility, the object is also a Thenable that is resolved
  1278. * when the window is done closing, see T163510.
  1279. * @fires closing
  1280. */
  1281. OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
  1282. var error,
  1283. manager = this,
  1284. compatClosing = $.Deferred(),
  1285. lifecycle = this.lifecycle,
  1286. compatOpened;
  1287. // Argument handling
  1288. if ( typeof win === 'string' ) {
  1289. win = this.windows[ win ];
  1290. } else if ( !this.hasWindow( win ) ) {
  1291. win = null;
  1292. }
  1293. // Error handling
  1294. if ( !lifecycle ) {
  1295. error = 'Cannot close window: no window is currently open';
  1296. } else if ( !win ) {
  1297. error = 'Cannot close window: window is not attached to manager';
  1298. } else if ( win !== this.currentWindow || this.lifecycle.isClosed() ) {
  1299. error = 'Cannot close window: window already closed with different data';
  1300. } else if ( this.preparingToClose || this.lifecycle.isClosing() ) {
  1301. error = 'Cannot close window: window already closing with different data';
  1302. }
  1303. if ( error ) {
  1304. // This function was called for the wrong window and we don't want to mess with the current
  1305. // window's state.
  1306. lifecycle = new OO.ui.WindowInstance();
  1307. // Pretend the window has been opened, so that we can pretend to fail to close it.
  1308. lifecycle.deferreds.opening.resolve( {} );
  1309. lifecycle.deferreds.opened.resolve( {} );
  1310. }
  1311. // Turn lifecycle into a Thenable for backwards-compatibility with
  1312. // the deprecated nested-promise behaviour, see T163510.
  1313. [ 'state', 'always', 'catch', 'pipe', 'then', 'promise', 'progress', 'done', 'fail' ]
  1314. .forEach( function ( method ) {
  1315. lifecycle[ method ] = function () {
  1316. OO.ui.warnDeprecation(
  1317. 'Using the return value of closeWindow as a promise is deprecated. ' +
  1318. 'Use .closeWindow( ... ).closed.' + method + '( ... ) instead.'
  1319. );
  1320. return compatClosing[ method ].apply( this, arguments );
  1321. };
  1322. } );
  1323. if ( error ) {
  1324. compatClosing.reject( new OO.ui.Error( error ) );
  1325. lifecycle.deferreds.closing.reject( new OO.ui.Error( error ) );
  1326. return lifecycle;
  1327. }
  1328. // If the window is currently opening, close it when it's done
  1329. this.preparingToClose = $.when( this.lifecycle.opened );
  1330. // Ensure handlers get called after preparingToClose is set
  1331. this.preparingToClose.always( function () {
  1332. manager.preparingToClose = null;
  1333. manager.emit( 'closing', win, compatClosing, data );
  1334. lifecycle.deferreds.closing.resolve( data );
  1335. compatOpened = manager.compatOpened;
  1336. manager.compatOpened = null;
  1337. compatOpened.resolve( compatClosing.promise(), data );
  1338. setTimeout( function () {
  1339. win.hold( data ).then( function () {
  1340. compatClosing.notify( { state: 'hold' } );
  1341. setTimeout( function () {
  1342. win.teardown( data ).then( function () {
  1343. compatClosing.notify( { state: 'teardown' } );
  1344. if ( manager.modal ) {
  1345. manager.toggleGlobalEvents( false );
  1346. manager.toggleAriaIsolation( false );
  1347. }
  1348. if ( manager.$returnFocusTo && manager.$returnFocusTo.length ) {
  1349. manager.$returnFocusTo[ 0 ].focus();
  1350. }
  1351. manager.currentWindow = null;
  1352. manager.lifecycle = null;
  1353. lifecycle.deferreds.closed.resolve( data );
  1354. compatClosing.resolve( data );
  1355. } );
  1356. }, manager.getTeardownDelay() );
  1357. } );
  1358. }, manager.getHoldDelay() );
  1359. } );
  1360. return lifecycle;
  1361. };
  1362. /**
  1363. * Add windows to the window manager.
  1364. *
  1365. * Windows can be added by reference, symbolic name, or explicitly defined symbolic names.
  1366. * See the [OOUI documentation on MediaWiki] [2] for examples.
  1367. * [2]: https://www.mediawiki.org/wiki/OOUI/Windows/Window_managers
  1368. *
  1369. * This function can be called in two manners:
  1370. *
  1371. * 1. `.addWindows( [ windowA, windowB, ... ] )` (where `windowA`, `windowB` are OO.ui.Window objects)
  1372. *
  1373. * This syntax registers windows under the symbolic names defined in their `.static.name`
  1374. * properties. For example, if `windowA.constructor.static.name` is `'nameA'`, calling
  1375. * `.openWindow( 'nameA' )` afterwards will open the window `windowA`. This syntax requires the
  1376. * static name to be set, otherwise an exception will be thrown.
  1377. *
  1378. * This is the recommended way, as it allows for an easier switch to using a window factory.
  1379. *
  1380. * 2. `.addWindows( { nameA: windowA, nameB: windowB, ... } )`
  1381. *
  1382. * This syntax registers windows under the explicitly given symbolic names. In this example,
  1383. * calling `.openWindow( 'nameA' )` afterwards will open the window `windowA`, regardless of what
  1384. * its `.static.name` is set to. The static name is not required to be set.
  1385. *
  1386. * This should only be used if you need to override the default symbolic names.
  1387. *
  1388. * Example:
  1389. *
  1390. * var windowManager = new OO.ui.WindowManager();
  1391. * $( 'body' ).append( windowManager.$element );
  1392. *
  1393. * // Add a window under the default name: see OO.ui.MessageDialog.static.name
  1394. * windowManager.addWindows( [ new OO.ui.MessageDialog() ] );
  1395. * // Add a window under an explicit name
  1396. * windowManager.addWindows( { myMessageDialog: new OO.ui.MessageDialog() } );
  1397. *
  1398. * // Open window by default name
  1399. * windowManager.openWindow( 'message' );
  1400. * // Open window by explicitly given name
  1401. * windowManager.openWindow( 'myMessageDialog' );
  1402. *
  1403. *
  1404. * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows An array of window objects specified
  1405. * by reference, symbolic name, or explicitly defined symbolic names.
  1406. * @throws {Error} An error is thrown if a window is added by symbolic name, but has neither an
  1407. * explicit nor a statically configured symbolic name.
  1408. */
  1409. OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
  1410. var i, len, win, name, list;
  1411. if ( Array.isArray( windows ) ) {
  1412. // Convert to map of windows by looking up symbolic names from static configuration
  1413. list = {};
  1414. for ( i = 0, len = windows.length; i < len; i++ ) {
  1415. name = windows[ i ].constructor.static.name;
  1416. if ( !name ) {
  1417. throw new Error( 'Windows must have a `name` static property defined.' );
  1418. }
  1419. list[ name ] = windows[ i ];
  1420. }
  1421. } else if ( OO.isPlainObject( windows ) ) {
  1422. list = windows;
  1423. }
  1424. // Add windows
  1425. for ( name in list ) {
  1426. win = list[ name ];
  1427. this.windows[ name ] = win.toggle( false );
  1428. this.$element.append( win.$element );
  1429. win.setManager( this );
  1430. }
  1431. };
  1432. /**
  1433. * Remove the specified windows from the windows manager.
  1434. *
  1435. * Windows will be closed before they are removed. If you wish to remove all windows, you may wish to use
  1436. * the #clearWindows method instead. If you no longer need the window manager and want to ensure that it no
  1437. * longer listens to events, use the #destroy method.
  1438. *
  1439. * @param {string[]} names Symbolic names of windows to remove
  1440. * @return {jQuery.Promise} Promise resolved when window is closed and removed
  1441. * @throws {Error} An error is thrown if the named windows are not managed by the window manager.
  1442. */
  1443. OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
  1444. var i, len, win, name, cleanupWindow,
  1445. manager = this,
  1446. promises = [],
  1447. cleanup = function ( name, win ) {
  1448. delete manager.windows[ name ];
  1449. win.$element.detach();
  1450. };
  1451. for ( i = 0, len = names.length; i < len; i++ ) {
  1452. name = names[ i ];
  1453. win = this.windows[ name ];
  1454. if ( !win ) {
  1455. throw new Error( 'Cannot remove window' );
  1456. }
  1457. cleanupWindow = cleanup.bind( null, name, win );
  1458. promises.push( this.closeWindow( name ).closed.then( cleanupWindow, cleanupWindow ) );
  1459. }
  1460. return $.when.apply( $, promises );
  1461. };
  1462. /**
  1463. * Remove all windows from the window manager.
  1464. *
  1465. * Windows will be closed before they are removed. Note that the window manager, though not in use, will still
  1466. * listen to events. If the window manager will not be used again, you may wish to use the #destroy method instead.
  1467. * To remove just a subset of windows, use the #removeWindows method.
  1468. *
  1469. * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
  1470. */
  1471. OO.ui.WindowManager.prototype.clearWindows = function () {
  1472. return this.removeWindows( Object.keys( this.windows ) );
  1473. };
  1474. /**
  1475. * Set dialog size. In general, this method should not be called directly.
  1476. *
  1477. * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
  1478. *
  1479. * @param {OO.ui.Window} win Window to update, should be the current window
  1480. * @chainable
  1481. */
  1482. OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
  1483. var isFullscreen;
  1484. // Bypass for non-current, and thus invisible, windows
  1485. if ( win !== this.currentWindow ) {
  1486. return;
  1487. }
  1488. isFullscreen = win.getSize() === 'full';
  1489. this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', isFullscreen );
  1490. this.$element.toggleClass( 'oo-ui-windowManager-floating', !isFullscreen );
  1491. win.setDimensions( win.getSizeProperties() );
  1492. this.emit( 'resize', win );
  1493. return this;
  1494. };
  1495. /**
  1496. * Bind or unbind global events for scrolling.
  1497. *
  1498. * @private
  1499. * @param {boolean} [on] Bind global events
  1500. * @chainable
  1501. */
  1502. OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
  1503. var scrollWidth, bodyMargin,
  1504. $body = $( this.getElementDocument().body ),
  1505. // We could have multiple window managers open so only modify
  1506. // the body css at the bottom of the stack
  1507. stackDepth = $body.data( 'windowManagerGlobalEvents' ) || 0;
  1508. on = on === undefined ? !!this.globalEvents : !!on;
  1509. if ( on ) {
  1510. if ( !this.globalEvents ) {
  1511. $( this.getElementWindow() ).on( {
  1512. // Start listening for top-level window dimension changes
  1513. 'orientationchange resize': this.onWindowResizeHandler
  1514. } );
  1515. if ( stackDepth === 0 ) {
  1516. scrollWidth = window.innerWidth - document.documentElement.clientWidth;
  1517. bodyMargin = parseFloat( $body.css( 'margin-right' ) ) || 0;
  1518. $body.css( {
  1519. overflow: 'hidden',
  1520. 'margin-right': bodyMargin + scrollWidth
  1521. } );
  1522. }
  1523. stackDepth++;
  1524. this.globalEvents = true;
  1525. }
  1526. } else if ( this.globalEvents ) {
  1527. $( this.getElementWindow() ).off( {
  1528. // Stop listening for top-level window dimension changes
  1529. 'orientationchange resize': this.onWindowResizeHandler
  1530. } );
  1531. stackDepth--;
  1532. if ( stackDepth === 0 ) {
  1533. $body.css( {
  1534. overflow: '',
  1535. 'margin-right': ''
  1536. } );
  1537. }
  1538. this.globalEvents = false;
  1539. }
  1540. $body.data( 'windowManagerGlobalEvents', stackDepth );
  1541. return this;
  1542. };
  1543. /**
  1544. * Toggle screen reader visibility of content other than the window manager.
  1545. *
  1546. * @private
  1547. * @param {boolean} [isolate] Make only the window manager visible to screen readers
  1548. * @chainable
  1549. */
  1550. OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) {
  1551. var $topLevelElement;
  1552. isolate = isolate === undefined ? !this.$ariaHidden : !!isolate;
  1553. if ( isolate ) {
  1554. if ( !this.$ariaHidden ) {
  1555. // Find the top level element containing the window manager or the
  1556. // window manager's element itself in case its a direct child of body
  1557. $topLevelElement = this.$element.parentsUntil( 'body' ).last();
  1558. $topLevelElement = $topLevelElement.length === 0 ? this.$element : $topLevelElement;
  1559. // In case previously set by another window manager
  1560. this.$element.removeAttr( 'aria-hidden' );
  1561. // Hide everything other than the window manager from screen readers
  1562. this.$ariaHidden = $( 'body' )
  1563. .children()
  1564. .not( 'script' )
  1565. .not( $topLevelElement )
  1566. .attr( 'aria-hidden', true );
  1567. }
  1568. } else if ( this.$ariaHidden ) {
  1569. // Restore screen reader visibility
  1570. this.$ariaHidden.removeAttr( 'aria-hidden' );
  1571. this.$ariaHidden = null;
  1572. // and hide the window manager
  1573. this.$element.attr( 'aria-hidden', true );
  1574. }
  1575. return this;
  1576. };
  1577. /**
  1578. * Destroy the window manager.
  1579. *
  1580. * Destroying the window manager ensures that it will no longer listen to events. If you would like to
  1581. * continue using the window manager, but wish to remove all windows from it, use the #clearWindows method
  1582. * instead.
  1583. */
  1584. OO.ui.WindowManager.prototype.destroy = function () {
  1585. this.toggleGlobalEvents( false );
  1586. this.toggleAriaIsolation( false );
  1587. this.clearWindows();
  1588. this.$element.remove();
  1589. };
  1590. /**
  1591. * A window is a container for elements that are in a child frame. They are used with
  1592. * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
  1593. * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’, ‘medium’,
  1594. * ‘large’), which is interpreted by the window manager. If the requested size is not recognized,
  1595. * the window manager will choose a sensible fallback.
  1596. *
  1597. * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
  1598. * different processes are executed:
  1599. *
  1600. * **opening**: The opening stage begins when the window manager's {@link OO.ui.WindowManager#openWindow
  1601. * openWindow} or the window's {@link #open open} methods are used, and the window manager begins to open
  1602. * the window.
  1603. *
  1604. * - {@link #getSetupProcess} method is called and its result executed
  1605. * - {@link #getReadyProcess} method is called and its result executed
  1606. *
  1607. * **opened**: The window is now open
  1608. *
  1609. * **closing**: The closing stage begins when the window manager's
  1610. * {@link OO.ui.WindowManager#closeWindow closeWindow}
  1611. * or the window's {@link #close} methods are used, and the window manager begins to close the window.
  1612. *
  1613. * - {@link #getHoldProcess} method is called and its result executed
  1614. * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
  1615. *
  1616. * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
  1617. * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and #getTeardownProcess
  1618. * methods. Note that each {@link OO.ui.Process process} is executed in series, so asynchronous
  1619. * processing can complete. Always assume window processes are executed asynchronously.
  1620. *
  1621. * For more information, please see the [OOUI documentation on MediaWiki] [1].
  1622. *
  1623. * [1]: https://www.mediawiki.org/wiki/OOUI/Windows
  1624. *
  1625. * @abstract
  1626. * @class
  1627. * @extends OO.ui.Element
  1628. * @mixins OO.EventEmitter
  1629. *
  1630. * @constructor
  1631. * @param {Object} [config] Configuration options
  1632. * @cfg {string} [size] Symbolic name of the dialog size: `small`, `medium`, `large`, `larger` or
  1633. * `full`. If omitted, the value of the {@link #static-size static size} property will be used.
  1634. */
  1635. OO.ui.Window = function OoUiWindow( config ) {
  1636. // Configuration initialization
  1637. config = config || {};
  1638. // Parent constructor
  1639. OO.ui.Window.parent.call( this, config );
  1640. // Mixin constructors
  1641. OO.EventEmitter.call( this );
  1642. // Properties
  1643. this.manager = null;
  1644. this.size = config.size || this.constructor.static.size;
  1645. this.$frame = $( '<div>' );
  1646. /**
  1647. * Overlay element to use for the `$overlay` configuration option of widgets that support it.
  1648. * Things put inside of it are overlaid on top of the window and are not bound to its dimensions.
  1649. * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
  1650. *
  1651. * MyDialog.prototype.initialize = function () {
  1652. * ...
  1653. * var popupButton = new OO.ui.PopupButtonWidget( {
  1654. * $overlay: this.$overlay,
  1655. * label: 'Popup button',
  1656. * popup: {
  1657. * $content: $( '<p>Popup contents.</p><p>Popup contents.</p><p>Popup contents.</p>' ),
  1658. * padded: true
  1659. * }
  1660. * } );
  1661. * ...
  1662. * };
  1663. *
  1664. * @property {jQuery}
  1665. */
  1666. this.$overlay = $( '<div>' );
  1667. this.$content = $( '<div>' );
  1668. this.$focusTrapBefore = $( '<div>' ).prop( 'tabIndex', 0 );
  1669. this.$focusTrapAfter = $( '<div>' ).prop( 'tabIndex', 0 );
  1670. this.$focusTraps = this.$focusTrapBefore.add( this.$focusTrapAfter );
  1671. // Initialization
  1672. this.$overlay.addClass( 'oo-ui-window-overlay' );
  1673. this.$content
  1674. .addClass( 'oo-ui-window-content' )
  1675. .attr( 'tabindex', 0 );
  1676. this.$frame
  1677. .addClass( 'oo-ui-window-frame' )
  1678. .append( this.$focusTrapBefore, this.$content, this.$focusTrapAfter );
  1679. this.$element
  1680. .addClass( 'oo-ui-window' )
  1681. .append( this.$frame, this.$overlay );
  1682. // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
  1683. // that reference properties not initialized at that time of parent class construction
  1684. // TODO: Find a better way to handle post-constructor setup
  1685. this.visible = false;
  1686. this.$element.addClass( 'oo-ui-element-hidden' );
  1687. };
  1688. /* Setup */
  1689. OO.inheritClass( OO.ui.Window, OO.ui.Element );
  1690. OO.mixinClass( OO.ui.Window, OO.EventEmitter );
  1691. /* Static Properties */
  1692. /**
  1693. * Symbolic name of the window size: `small`, `medium`, `large`, `larger` or `full`.
  1694. *
  1695. * The static size is used if no #size is configured during construction.
  1696. *
  1697. * @static
  1698. * @inheritable
  1699. * @property {string}
  1700. */
  1701. OO.ui.Window.static.size = 'medium';
  1702. /* Methods */
  1703. /**
  1704. * Handle mouse down events.
  1705. *
  1706. * @private
  1707. * @param {jQuery.Event} e Mouse down event
  1708. */
  1709. OO.ui.Window.prototype.onMouseDown = function ( e ) {
  1710. // Prevent clicking on the click-block from stealing focus
  1711. if ( e.target === this.$element[ 0 ] ) {
  1712. return false;
  1713. }
  1714. };
  1715. /**
  1716. * Check if the window has been initialized.
  1717. *
  1718. * Initialization occurs when a window is added to a manager.
  1719. *
  1720. * @return {boolean} Window has been initialized
  1721. */
  1722. OO.ui.Window.prototype.isInitialized = function () {
  1723. return !!this.manager;
  1724. };
  1725. /**
  1726. * Check if the window is visible.
  1727. *
  1728. * @return {boolean} Window is visible
  1729. */
  1730. OO.ui.Window.prototype.isVisible = function () {
  1731. return this.visible;
  1732. };
  1733. /**
  1734. * Check if the window is opening.
  1735. *
  1736. * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpening isOpening}
  1737. * method.
  1738. *
  1739. * @return {boolean} Window is opening
  1740. */
  1741. OO.ui.Window.prototype.isOpening = function () {
  1742. return this.manager.isOpening( this );
  1743. };
  1744. /**
  1745. * Check if the window is closing.
  1746. *
  1747. * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isClosing isClosing} method.
  1748. *
  1749. * @return {boolean} Window is closing
  1750. */
  1751. OO.ui.Window.prototype.isClosing = function () {
  1752. return this.manager.isClosing( this );
  1753. };
  1754. /**
  1755. * Check if the window is opened.
  1756. *
  1757. * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpened isOpened} method.
  1758. *
  1759. * @return {boolean} Window is opened
  1760. */
  1761. OO.ui.Window.prototype.isOpened = function () {
  1762. return this.manager.isOpened( this );
  1763. };
  1764. /**
  1765. * Get the window manager.
  1766. *
  1767. * All windows must be attached to a window manager, which is used to open
  1768. * and close the window and control its presentation.
  1769. *
  1770. * @return {OO.ui.WindowManager} Manager of window
  1771. */
  1772. OO.ui.Window.prototype.getManager = function () {
  1773. return this.manager;
  1774. };
  1775. /**
  1776. * Get the symbolic name of the window size (e.g., `small` or `medium`).
  1777. *
  1778. * @return {string} Symbolic name of the size: `small`, `medium`, `large`, `larger`, `full`
  1779. */
  1780. OO.ui.Window.prototype.getSize = function () {
  1781. var viewport = OO.ui.Element.static.getDimensions( this.getElementWindow() ),
  1782. sizes = this.manager.constructor.static.sizes,
  1783. size = this.size;
  1784. if ( !sizes[ size ] ) {
  1785. size = this.manager.constructor.static.defaultSize;
  1786. }
  1787. if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) {
  1788. size = 'full';
  1789. }
  1790. return size;
  1791. };
  1792. /**
  1793. * Get the size properties associated with the current window size
  1794. *
  1795. * @return {Object} Size properties
  1796. */
  1797. OO.ui.Window.prototype.getSizeProperties = function () {
  1798. return this.manager.constructor.static.sizes[ this.getSize() ];
  1799. };
  1800. /**
  1801. * Disable transitions on window's frame for the duration of the callback function, then enable them
  1802. * back.
  1803. *
  1804. * @private
  1805. * @param {Function} callback Function to call while transitions are disabled
  1806. */
  1807. OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) {
  1808. // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
  1809. // Disable transitions first, otherwise we'll get values from when the window was animating.
  1810. // We need to build the transition CSS properties using these specific properties since
  1811. // Firefox doesn't return anything useful when asked just for 'transition'.
  1812. var oldTransition = this.$frame.css( 'transition-property' ) + ' ' +
  1813. this.$frame.css( 'transition-duration' ) + ' ' +
  1814. this.$frame.css( 'transition-timing-function' ) + ' ' +
  1815. this.$frame.css( 'transition-delay' );
  1816. this.$frame.css( 'transition', 'none' );
  1817. callback();
  1818. // Force reflow to make sure the style changes done inside callback
  1819. // really are not transitioned
  1820. this.$frame.height();
  1821. this.$frame.css( 'transition', oldTransition );
  1822. };
  1823. /**
  1824. * Get the height of the full window contents (i.e., the window head, body and foot together).
  1825. *
  1826. * What consistitutes the head, body, and foot varies depending on the window type.
  1827. * A {@link OO.ui.MessageDialog message dialog} displays a title and message in its body,
  1828. * and any actions in the foot. A {@link OO.ui.ProcessDialog process dialog} displays a title
  1829. * and special actions in the head, and dialog content in the body.
  1830. *
  1831. * To get just the height of the dialog body, use the #getBodyHeight method.
  1832. *
  1833. * @return {number} The height of the window contents (the dialog head, body and foot) in pixels
  1834. */
  1835. OO.ui.Window.prototype.getContentHeight = function () {
  1836. var bodyHeight,
  1837. win = this,
  1838. bodyStyleObj = this.$body[ 0 ].style,
  1839. frameStyleObj = this.$frame[ 0 ].style;
  1840. // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
  1841. // Disable transitions first, otherwise we'll get values from when the window was animating.
  1842. this.withoutSizeTransitions( function () {
  1843. var oldHeight = frameStyleObj.height,
  1844. oldPosition = bodyStyleObj.position;
  1845. frameStyleObj.height = '1px';
  1846. // Force body to resize to new width
  1847. bodyStyleObj.position = 'relative';
  1848. bodyHeight = win.getBodyHeight();
  1849. frameStyleObj.height = oldHeight;
  1850. bodyStyleObj.position = oldPosition;
  1851. } );
  1852. return (
  1853. // Add buffer for border
  1854. ( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
  1855. // Use combined heights of children
  1856. ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) )
  1857. );
  1858. };
  1859. /**
  1860. * Get the height of the window body.
  1861. *
  1862. * To get the height of the full window contents (the window body, head, and foot together),
  1863. * use #getContentHeight.
  1864. *
  1865. * When this function is called, the window will temporarily have been resized
  1866. * to height=1px, so .scrollHeight measurements can be taken accurately.
  1867. *
  1868. * @return {number} Height of the window body in pixels
  1869. */
  1870. OO.ui.Window.prototype.getBodyHeight = function () {
  1871. return this.$body[ 0 ].scrollHeight;
  1872. };
  1873. /**
  1874. * Get the directionality of the frame (right-to-left or left-to-right).
  1875. *
  1876. * @return {string} Directionality: `'ltr'` or `'rtl'`
  1877. */
  1878. OO.ui.Window.prototype.getDir = function () {
  1879. return OO.ui.Element.static.getDir( this.$content ) || 'ltr';
  1880. };
  1881. /**
  1882. * Get the 'setup' process.
  1883. *
  1884. * The setup process is used to set up a window for use in a particular context, based on the `data`
  1885. * argument. This method is called during the opening phase of the window’s lifecycle (before the
  1886. * opening animation). You can add elements to the window in this process or set their default
  1887. * values.
  1888. *
  1889. * Override this method to add additional steps to the ‘setup’ process the parent method provides
  1890. * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
  1891. * of OO.ui.Process.
  1892. *
  1893. * To add window content that persists between openings, you may wish to use the #initialize method
  1894. * instead.
  1895. *
  1896. * @param {Object} [data] Window opening data
  1897. * @return {OO.ui.Process} Setup process
  1898. */
  1899. OO.ui.Window.prototype.getSetupProcess = function () {
  1900. return new OO.ui.Process();
  1901. };
  1902. /**
  1903. * Get the ‘ready’ process.
  1904. *
  1905. * The ready process is used to ready a window for use in a particular context, based on the `data`
  1906. * argument. This method is called during the opening phase of the window’s lifecycle, after the
  1907. * window has been {@link #getSetupProcess setup} (after the opening animation). You can focus
  1908. * elements in the window in this process, or open their dropdowns.
  1909. *
  1910. * Override this method to add additional steps to the ‘ready’ process the parent method
  1911. * provides using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next}
  1912. * methods of OO.ui.Process.
  1913. *
  1914. * @param {Object} [data] Window opening data
  1915. * @return {OO.ui.Process} Ready process
  1916. */
  1917. OO.ui.Window.prototype.getReadyProcess = function () {
  1918. return new OO.ui.Process();
  1919. };
  1920. /**
  1921. * Get the 'hold' process.
  1922. *
  1923. * The hold process is used to keep a window from being used in a particular context, based on the
  1924. * `data` argument. This method is called during the closing phase of the window’s lifecycle (before
  1925. * the closing animation). You can close dropdowns of elements in the window in this process, if
  1926. * they do not get closed automatically.
  1927. *
  1928. * Override this method to add additional steps to the 'hold' process the parent method provides
  1929. * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
  1930. * of OO.ui.Process.
  1931. *
  1932. * @param {Object} [data] Window closing data
  1933. * @return {OO.ui.Process} Hold process
  1934. */
  1935. OO.ui.Window.prototype.getHoldProcess = function () {
  1936. return new OO.ui.Process();
  1937. };
  1938. /**
  1939. * Get the ‘teardown’ process.
  1940. *
  1941. * The teardown process is used to teardown a window after use. During teardown, user interactions
  1942. * within the window are conveyed and the window is closed, based on the `data` argument. This
  1943. * method is called during the closing phase of the window’s lifecycle (after the closing
  1944. * animation). You can remove elements in the window in this process or clear their values.
  1945. *
  1946. * Override this method to add additional steps to the ‘teardown’ process the parent method provides
  1947. * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
  1948. * of OO.ui.Process.
  1949. *
  1950. * @param {Object} [data] Window closing data
  1951. * @return {OO.ui.Process} Teardown process
  1952. */
  1953. OO.ui.Window.prototype.getTeardownProcess = function () {
  1954. return new OO.ui.Process();
  1955. };
  1956. /**
  1957. * Set the window manager.
  1958. *
  1959. * This will cause the window to initialize. Calling it more than once will cause an error.
  1960. *
  1961. * @param {OO.ui.WindowManager} manager Manager for this window
  1962. * @throws {Error} An error is thrown if the method is called more than once
  1963. * @chainable
  1964. */
  1965. OO.ui.Window.prototype.setManager = function ( manager ) {
  1966. if ( this.manager ) {
  1967. throw new Error( 'Cannot set window manager, window already has a manager' );
  1968. }
  1969. this.manager = manager;
  1970. this.initialize();
  1971. return this;
  1972. };
  1973. /**
  1974. * Set the window size by symbolic name (e.g., 'small' or 'medium')
  1975. *
  1976. * @param {string} size Symbolic name of size: `small`, `medium`, `large`, `larger` or
  1977. * `full`
  1978. * @chainable
  1979. */
  1980. OO.ui.Window.prototype.setSize = function ( size ) {
  1981. this.size = size;
  1982. this.updateSize();
  1983. return this;
  1984. };
  1985. /**
  1986. * Update the window size.
  1987. *
  1988. * @throws {Error} An error is thrown if the window is not attached to a window manager
  1989. * @chainable
  1990. */
  1991. OO.ui.Window.prototype.updateSize = function () {
  1992. if ( !this.manager ) {
  1993. throw new Error( 'Cannot update window size, must be attached to a manager' );
  1994. }
  1995. this.manager.updateWindowSize( this );
  1996. return this;
  1997. };
  1998. /**
  1999. * Set window dimensions. This method is called by the {@link OO.ui.WindowManager window manager}
  2000. * when the window is opening. In general, setDimensions should not be called directly.
  2001. *
  2002. * To set the size of the window, use the #setSize method.
  2003. *
  2004. * @param {Object} dim CSS dimension properties
  2005. * @param {string|number} [dim.width] Width
  2006. * @param {string|number} [dim.minWidth] Minimum width
  2007. * @param {string|number} [dim.maxWidth] Maximum width
  2008. * @param {string|number} [dim.height] Height, omit to set based on height of contents
  2009. * @param {string|number} [dim.minHeight] Minimum height
  2010. * @param {string|number} [dim.maxHeight] Maximum height
  2011. * @chainable
  2012. */
  2013. OO.ui.Window.prototype.setDimensions = function ( dim ) {
  2014. var height,
  2015. win = this,
  2016. styleObj = this.$frame[ 0 ].style;
  2017. // Calculate the height we need to set using the correct width
  2018. if ( dim.height === undefined ) {
  2019. this.withoutSizeTransitions( function () {
  2020. var oldWidth = styleObj.width;
  2021. win.$frame.css( 'width', dim.width || '' );
  2022. height = win.getContentHeight();
  2023. styleObj.width = oldWidth;
  2024. } );
  2025. } else {
  2026. height = dim.height;
  2027. }
  2028. this.$frame.css( {
  2029. width: dim.width || '',
  2030. minWidth: dim.minWidth || '',
  2031. maxWidth: dim.maxWidth || '',
  2032. height: height || '',
  2033. minHeight: dim.minHeight || '',
  2034. maxHeight: dim.maxHeight || ''
  2035. } );
  2036. return this;
  2037. };
  2038. /**
  2039. * Initialize window contents.
  2040. *
  2041. * Before the window is opened for the first time, #initialize is called so that content that
  2042. * persists between openings can be added to the window.
  2043. *
  2044. * To set up a window with new content each time the window opens, use #getSetupProcess.
  2045. *
  2046. * @throws {Error} An error is thrown if the window is not attached to a window manager
  2047. * @chainable
  2048. */
  2049. OO.ui.Window.prototype.initialize = function () {
  2050. if ( !this.manager ) {
  2051. throw new Error( 'Cannot initialize window, must be attached to a manager' );
  2052. }
  2053. // Properties
  2054. this.$head = $( '<div>' );
  2055. this.$body = $( '<div>' );
  2056. this.$foot = $( '<div>' );
  2057. this.$document = $( this.getElementDocument() );
  2058. // Events
  2059. this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
  2060. // Initialization
  2061. this.$head.addClass( 'oo-ui-window-head' );
  2062. this.$body.addClass( 'oo-ui-window-body' );
  2063. this.$foot.addClass( 'oo-ui-window-foot' );
  2064. this.$content.append( this.$head, this.$body, this.$foot );
  2065. return this;
  2066. };
  2067. /**
  2068. * Called when someone tries to focus the hidden element at the end of the dialog.
  2069. * Sends focus back to the start of the dialog.
  2070. *
  2071. * @param {jQuery.Event} event Focus event
  2072. */
  2073. OO.ui.Window.prototype.onFocusTrapFocused = function ( event ) {
  2074. var backwards = this.$focusTrapBefore.is( event.target ),
  2075. element = OO.ui.findFocusable( this.$content, backwards );
  2076. if ( element ) {
  2077. // There's a focusable element inside the content, at the front or
  2078. // back depending on which focus trap we hit; select it.
  2079. element.focus();
  2080. } else {
  2081. // There's nothing focusable inside the content. As a fallback,
  2082. // this.$content is focusable, and focusing it will keep our focus
  2083. // properly trapped. It's not a *meaningful* focus, since it's just
  2084. // the content-div for the Window, but it's better than letting focus
  2085. // escape into the page.
  2086. this.$content.focus();
  2087. }
  2088. };
  2089. /**
  2090. * Open the window.
  2091. *
  2092. * This method is a wrapper around a call to the window
  2093. * manager’s {@link OO.ui.WindowManager#openWindow openWindow} method.
  2094. *
  2095. * To customize the window each time it opens, use #getSetupProcess or #getReadyProcess.
  2096. *
  2097. * @param {Object} [data] Window opening data
  2098. * @return {OO.ui.WindowInstance} See OO.ui.WindowManager#openWindow
  2099. * @throws {Error} An error is thrown if the window is not attached to a window manager
  2100. */
  2101. OO.ui.Window.prototype.open = function ( data ) {
  2102. if ( !this.manager ) {
  2103. throw new Error( 'Cannot open window, must be attached to a manager' );
  2104. }
  2105. return this.manager.openWindow( this, data );
  2106. };
  2107. /**
  2108. * Close the window.
  2109. *
  2110. * This method is a wrapper around a call to the window
  2111. * manager’s {@link OO.ui.WindowManager#closeWindow closeWindow} method.
  2112. *
  2113. * The window's #getHoldProcess and #getTeardownProcess methods are called during the closing
  2114. * phase of the window’s lifecycle and can be used to specify closing behavior each time
  2115. * the window closes.
  2116. *
  2117. * @param {Object} [data] Window closing data
  2118. * @return {OO.ui.WindowInstance} See OO.ui.WindowManager#closeWindow
  2119. * @throws {Error} An error is thrown if the window is not attached to a window manager
  2120. */
  2121. OO.ui.Window.prototype.close = function ( data ) {
  2122. if ( !this.manager ) {
  2123. throw new Error( 'Cannot close window, must be attached to a manager' );
  2124. }
  2125. return this.manager.closeWindow( this, data );
  2126. };
  2127. /**
  2128. * Setup window.
  2129. *
  2130. * This is called by OO.ui.WindowManager during window opening (before the animation), and should
  2131. * not be called directly by other systems.
  2132. *
  2133. * @param {Object} [data] Window opening data
  2134. * @return {jQuery.Promise} Promise resolved when window is setup
  2135. */
  2136. OO.ui.Window.prototype.setup = function ( data ) {
  2137. var win = this;
  2138. this.toggle( true );
  2139. this.focusTrapHandler = OO.ui.bind( this.onFocusTrapFocused, this );
  2140. this.$focusTraps.on( 'focus', this.focusTrapHandler );
  2141. return this.getSetupProcess( data ).execute().then( function () {
  2142. win.updateSize();
  2143. // Force redraw by asking the browser to measure the elements' widths
  2144. win.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
  2145. win.$content.addClass( 'oo-ui-window-content-setup' ).width();
  2146. } );
  2147. };
  2148. /**
  2149. * Ready window.
  2150. *
  2151. * This is called by OO.ui.WindowManager during window opening (after the animation), and should not
  2152. * be called directly by other systems.
  2153. *
  2154. * @param {Object} [data] Window opening data
  2155. * @return {jQuery.Promise} Promise resolved when window is ready
  2156. */
  2157. OO.ui.Window.prototype.ready = function ( data ) {
  2158. var win = this;
  2159. this.$content.focus();
  2160. return this.getReadyProcess( data ).execute().then( function () {
  2161. // Force redraw by asking the browser to measure the elements' widths
  2162. win.$element.addClass( 'oo-ui-window-ready' ).width();
  2163. win.$content.addClass( 'oo-ui-window-content-ready' ).width();
  2164. } );
  2165. };
  2166. /**
  2167. * Hold window.
  2168. *
  2169. * This is called by OO.ui.WindowManager during window closing (before the animation), and should
  2170. * not be called directly by other systems.
  2171. *
  2172. * @param {Object} [data] Window closing data
  2173. * @return {jQuery.Promise} Promise resolved when window is held
  2174. */
  2175. OO.ui.Window.prototype.hold = function ( data ) {
  2176. var win = this;
  2177. return this.getHoldProcess( data ).execute().then( function () {
  2178. // Get the focused element within the window's content
  2179. var $focus = win.$content.find( OO.ui.Element.static.getDocument( win.$content ).activeElement );
  2180. // Blur the focused element
  2181. if ( $focus.length ) {
  2182. $focus[ 0 ].blur();
  2183. }
  2184. // Force redraw by asking the browser to measure the elements' widths
  2185. win.$element.removeClass( 'oo-ui-window-ready oo-ui-window-setup' ).width();
  2186. win.$content.removeClass( 'oo-ui-window-content-ready oo-ui-window-content-setup' ).width();
  2187. } );
  2188. };
  2189. /**
  2190. * Teardown window.
  2191. *
  2192. * This is called by OO.ui.WindowManager during window closing (after the animation), and should not be called directly
  2193. * by other systems.
  2194. *
  2195. * @param {Object} [data] Window closing data
  2196. * @return {jQuery.Promise} Promise resolved when window is torn down
  2197. */
  2198. OO.ui.Window.prototype.teardown = function ( data ) {
  2199. var win = this;
  2200. return this.getTeardownProcess( data ).execute().then( function () {
  2201. // Force redraw by asking the browser to measure the elements' widths
  2202. win.$element.removeClass( 'oo-ui-window-active' ).width();
  2203. win.$focusTraps.off( 'focus', win.focusTrapHandler );
  2204. win.toggle( false );
  2205. } );
  2206. };
  2207. /**
  2208. * The Dialog class serves as the base class for the other types of dialogs.
  2209. * Unless extended to include controls, the rendered dialog box is a simple window
  2210. * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager,
  2211. * which opens, closes, and controls the presentation of the window. See the
  2212. * [OOUI documentation on MediaWiki] [1] for more information.
  2213. *
  2214. * @example
  2215. * // A simple dialog window.
  2216. * function MyDialog( config ) {
  2217. * MyDialog.parent.call( this, config );
  2218. * }
  2219. * OO.inheritClass( MyDialog, OO.ui.Dialog );
  2220. * MyDialog.static.name = 'myDialog';
  2221. * MyDialog.prototype.initialize = function () {
  2222. * MyDialog.parent.prototype.initialize.call( this );
  2223. * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
  2224. * this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
  2225. * this.$body.append( this.content.$element );
  2226. * };
  2227. * MyDialog.prototype.getBodyHeight = function () {
  2228. * return this.content.$element.outerHeight( true );
  2229. * };
  2230. * var myDialog = new MyDialog( {
  2231. * size: 'medium'
  2232. * } );
  2233. * // Create and append a window manager, which opens and closes the window.
  2234. * var windowManager = new OO.ui.WindowManager();
  2235. * $( 'body' ).append( windowManager.$element );
  2236. * windowManager.addWindows( [ myDialog ] );
  2237. * // Open the window!
  2238. * windowManager.openWindow( myDialog );
  2239. *
  2240. * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Dialogs
  2241. *
  2242. * @abstract
  2243. * @class
  2244. * @extends OO.ui.Window
  2245. * @mixins OO.ui.mixin.PendingElement
  2246. *
  2247. * @constructor
  2248. * @param {Object} [config] Configuration options
  2249. */
  2250. OO.ui.Dialog = function OoUiDialog( config ) {
  2251. // Parent constructor
  2252. OO.ui.Dialog.parent.call( this, config );
  2253. // Mixin constructors
  2254. OO.ui.mixin.PendingElement.call( this );
  2255. // Properties
  2256. this.actions = new OO.ui.ActionSet();
  2257. this.attachedActions = [];
  2258. this.currentAction = null;
  2259. this.onDialogKeyDownHandler = this.onDialogKeyDown.bind( this );
  2260. // Events
  2261. this.actions.connect( this, {
  2262. click: 'onActionClick',
  2263. change: 'onActionsChange'
  2264. } );
  2265. // Initialization
  2266. this.$element
  2267. .addClass( 'oo-ui-dialog' )
  2268. .attr( 'role', 'dialog' );
  2269. };
  2270. /* Setup */
  2271. OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
  2272. OO.mixinClass( OO.ui.Dialog, OO.ui.mixin.PendingElement );
  2273. /* Static Properties */
  2274. /**
  2275. * Symbolic name of dialog.
  2276. *
  2277. * The dialog class must have a symbolic name in order to be registered with OO.Factory.
  2278. * Please see the [OOUI documentation on MediaWiki] [3] for more information.
  2279. *
  2280. * [3]: https://www.mediawiki.org/wiki/OOUI/Windows/Window_managers
  2281. *
  2282. * @abstract
  2283. * @static
  2284. * @inheritable
  2285. * @property {string}
  2286. */
  2287. OO.ui.Dialog.static.name = '';
  2288. /**
  2289. * The dialog title.
  2290. *
  2291. * The title can be specified as a plaintext string, a {@link OO.ui.mixin.LabelElement Label} node, or a function
  2292. * that will produce a Label node or string. The title can also be specified with data passed to the
  2293. * constructor (see #getSetupProcess). In this case, the static value will be overridden.
  2294. *
  2295. * @abstract
  2296. * @static
  2297. * @inheritable
  2298. * @property {jQuery|string|Function}
  2299. */
  2300. OO.ui.Dialog.static.title = '';
  2301. /**
  2302. * An array of configured {@link OO.ui.ActionWidget action widgets}.
  2303. *
  2304. * Actions can also be specified with data passed to the constructor (see #getSetupProcess). In this case, the static
  2305. * value will be overridden.
  2306. *
  2307. * [2]: https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs#Action_sets
  2308. *
  2309. * @static
  2310. * @inheritable
  2311. * @property {Object[]}
  2312. */
  2313. OO.ui.Dialog.static.actions = [];
  2314. /**
  2315. * Close the dialog when the 'Esc' key is pressed.
  2316. *
  2317. * @static
  2318. * @abstract
  2319. * @inheritable
  2320. * @property {boolean}
  2321. */
  2322. OO.ui.Dialog.static.escapable = true;
  2323. /* Methods */
  2324. /**
  2325. * Handle frame document key down events.
  2326. *
  2327. * @private
  2328. * @param {jQuery.Event} e Key down event
  2329. */
  2330. OO.ui.Dialog.prototype.onDialogKeyDown = function ( e ) {
  2331. var actions;
  2332. if ( e.which === OO.ui.Keys.ESCAPE && this.constructor.static.escapable ) {
  2333. this.executeAction( '' );
  2334. e.preventDefault();
  2335. e.stopPropagation();
  2336. } else if ( e.which === OO.ui.Keys.ENTER && ( e.ctrlKey || e.metaKey ) ) {
  2337. actions = this.actions.get( { flags: 'primary', visible: true, disabled: false } );
  2338. if ( actions.length > 0 ) {
  2339. this.executeAction( actions[ 0 ].getAction() );
  2340. e.preventDefault();
  2341. e.stopPropagation();
  2342. }
  2343. }
  2344. };
  2345. /**
  2346. * Handle action click events.
  2347. *
  2348. * @private
  2349. * @param {OO.ui.ActionWidget} action Action that was clicked
  2350. */
  2351. OO.ui.Dialog.prototype.onActionClick = function ( action ) {
  2352. if ( !this.isPending() ) {
  2353. this.executeAction( action.getAction() );
  2354. }
  2355. };
  2356. /**
  2357. * Handle actions change event.
  2358. *
  2359. * @private
  2360. */
  2361. OO.ui.Dialog.prototype.onActionsChange = function () {
  2362. this.detachActions();
  2363. if ( !this.isClosing() ) {
  2364. this.attachActions();
  2365. if ( !this.isOpening() ) {
  2366. // If the dialog is currently opening, this will be called automatically soon.
  2367. this.updateSize();
  2368. }
  2369. }
  2370. };
  2371. /**
  2372. * Get the set of actions used by the dialog.
  2373. *
  2374. * @return {OO.ui.ActionSet}
  2375. */
  2376. OO.ui.Dialog.prototype.getActions = function () {
  2377. return this.actions;
  2378. };
  2379. /**
  2380. * Get a process for taking action.
  2381. *
  2382. * When you override this method, you can create a new OO.ui.Process and return it, or add additional
  2383. * accept steps to the process the parent method provides using the {@link OO.ui.Process#first 'first'}
  2384. * and {@link OO.ui.Process#next 'next'} methods of OO.ui.Process.
  2385. *
  2386. * @param {string} [action] Symbolic name of action
  2387. * @return {OO.ui.Process} Action process
  2388. */
  2389. OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
  2390. return new OO.ui.Process()
  2391. .next( function () {
  2392. if ( !action ) {
  2393. // An empty action always closes the dialog without data, which should always be
  2394. // safe and make no changes
  2395. this.close();
  2396. }
  2397. }, this );
  2398. };
  2399. /**
  2400. * @inheritdoc
  2401. *
  2402. * @param {Object} [data] Dialog opening data
  2403. * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use
  2404. * the {@link #static-title static title}
  2405. * @param {Object[]} [data.actions] List of configuration options for each
  2406. * {@link OO.ui.ActionWidget action widget}, omit to use {@link #static-actions static actions}.
  2407. */
  2408. OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
  2409. data = data || {};
  2410. // Parent method
  2411. return OO.ui.Dialog.parent.prototype.getSetupProcess.call( this, data )
  2412. .next( function () {
  2413. var config = this.constructor.static,
  2414. actions = data.actions !== undefined ? data.actions : config.actions,
  2415. title = data.title !== undefined ? data.title : config.title;
  2416. this.title.setLabel( title ).setTitle( title );
  2417. this.actions.add( this.getActionWidgets( actions ) );
  2418. this.$element.on( 'keydown', this.onDialogKeyDownHandler );
  2419. }, this );
  2420. };
  2421. /**
  2422. * @inheritdoc
  2423. */
  2424. OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
  2425. // Parent method
  2426. return OO.ui.Dialog.parent.prototype.getTeardownProcess.call( this, data )
  2427. .first( function () {
  2428. this.$element.off( 'keydown', this.onDialogKeyDownHandler );
  2429. this.actions.clear();
  2430. this.currentAction = null;
  2431. }, this );
  2432. };
  2433. /**
  2434. * @inheritdoc
  2435. */
  2436. OO.ui.Dialog.prototype.initialize = function () {
  2437. // Parent method
  2438. OO.ui.Dialog.parent.prototype.initialize.call( this );
  2439. // Properties
  2440. this.title = new OO.ui.LabelWidget();
  2441. // Initialization
  2442. this.$content.addClass( 'oo-ui-dialog-content' );
  2443. this.$element.attr( 'aria-labelledby', this.title.getElementId() );
  2444. this.setPendingElement( this.$head );
  2445. };
  2446. /**
  2447. * Get action widgets from a list of configs
  2448. *
  2449. * @param {Object[]} actions Action widget configs
  2450. * @return {OO.ui.ActionWidget[]} Action widgets
  2451. */
  2452. OO.ui.Dialog.prototype.getActionWidgets = function ( actions ) {
  2453. var i, len, widgets = [];
  2454. for ( i = 0, len = actions.length; i < len; i++ ) {
  2455. widgets.push( this.getActionWidget( actions[ i ] ) );
  2456. }
  2457. return widgets;
  2458. };
  2459. /**
  2460. * Get action widget from config
  2461. *
  2462. * Override this method to change the action widget class used.
  2463. *
  2464. * @param {Object} config Action widget config
  2465. * @return {OO.ui.ActionWidget} Action widget
  2466. */
  2467. OO.ui.Dialog.prototype.getActionWidget = function ( config ) {
  2468. return new OO.ui.ActionWidget( this.getActionWidgetConfig( config ) );
  2469. };
  2470. /**
  2471. * Get action widget config
  2472. *
  2473. * Override this method to modify the action widget config
  2474. *
  2475. * @param {Object} config Initial action widget config
  2476. * @return {Object} Action widget config
  2477. */
  2478. OO.ui.Dialog.prototype.getActionWidgetConfig = function ( config ) {
  2479. return config;
  2480. };
  2481. /**
  2482. * Attach action actions.
  2483. *
  2484. * @protected
  2485. */
  2486. OO.ui.Dialog.prototype.attachActions = function () {
  2487. // Remember the list of potentially attached actions
  2488. this.attachedActions = this.actions.get();
  2489. };
  2490. /**
  2491. * Detach action actions.
  2492. *
  2493. * @protected
  2494. * @chainable
  2495. */
  2496. OO.ui.Dialog.prototype.detachActions = function () {
  2497. var i, len;
  2498. // Detach all actions that may have been previously attached
  2499. for ( i = 0, len = this.attachedActions.length; i < len; i++ ) {
  2500. this.attachedActions[ i ].$element.detach();
  2501. }
  2502. this.attachedActions = [];
  2503. };
  2504. /**
  2505. * Execute an action.
  2506. *
  2507. * @param {string} action Symbolic name of action to execute
  2508. * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
  2509. */
  2510. OO.ui.Dialog.prototype.executeAction = function ( action ) {
  2511. this.pushPending();
  2512. this.currentAction = action;
  2513. return this.getActionProcess( action ).execute()
  2514. .always( this.popPending.bind( this ) );
  2515. };
  2516. /**
  2517. * MessageDialogs display a confirmation or alert message. By default, the rendered dialog box
  2518. * consists of a header that contains the dialog title, a body with the message, and a footer that
  2519. * contains any {@link OO.ui.ActionWidget action widgets}. The MessageDialog class is the only type
  2520. * of {@link OO.ui.Dialog dialog} that is usually instantiated directly.
  2521. *
  2522. * There are two basic types of message dialogs, confirmation and alert:
  2523. *
  2524. * - **confirmation**: the dialog title describes what a progressive action will do and the message provides
  2525. * more details about the consequences.
  2526. * - **alert**: the dialog title describes which event occurred and the message provides more information
  2527. * about why the event occurred.
  2528. *
  2529. * The MessageDialog class specifies two actions: ‘accept’, the primary
  2530. * action (e.g., ‘ok’) and ‘reject,’ the safe action (e.g., ‘cancel’). Both will close the window,
  2531. * passing along the selected action.
  2532. *
  2533. * For more information and examples, please see the [OOUI documentation on MediaWiki][1].
  2534. *
  2535. * @example
  2536. * // Example: Creating and opening a message dialog window.
  2537. * var messageDialog = new OO.ui.MessageDialog();
  2538. *
  2539. * // Create and append a window manager.
  2540. * var windowManager = new OO.ui.WindowManager();
  2541. * $( 'body' ).append( windowManager.$element );
  2542. * windowManager.addWindows( [ messageDialog ] );
  2543. * // Open the window.
  2544. * windowManager.openWindow( messageDialog, {
  2545. * title: 'Basic message dialog',
  2546. * message: 'This is the message'
  2547. * } );
  2548. *
  2549. * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Message_Dialogs
  2550. *
  2551. * @class
  2552. * @extends OO.ui.Dialog
  2553. *
  2554. * @constructor
  2555. * @param {Object} [config] Configuration options
  2556. */
  2557. OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
  2558. // Parent constructor
  2559. OO.ui.MessageDialog.parent.call( this, config );
  2560. // Properties
  2561. this.verticalActionLayout = null;
  2562. // Initialization
  2563. this.$element.addClass( 'oo-ui-messageDialog' );
  2564. };
  2565. /* Setup */
  2566. OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
  2567. /* Static Properties */
  2568. /**
  2569. * @static
  2570. * @inheritdoc
  2571. */
  2572. OO.ui.MessageDialog.static.name = 'message';
  2573. /**
  2574. * @static
  2575. * @inheritdoc
  2576. */
  2577. OO.ui.MessageDialog.static.size = 'small';
  2578. /**
  2579. * Dialog title.
  2580. *
  2581. * The title of a confirmation dialog describes what a progressive action will do. The
  2582. * title of an alert dialog describes which event occurred.
  2583. *
  2584. * @static
  2585. * @inheritable
  2586. * @property {jQuery|string|Function|null}
  2587. */
  2588. OO.ui.MessageDialog.static.title = null;
  2589. /**
  2590. * The message displayed in the dialog body.
  2591. *
  2592. * A confirmation message describes the consequences of a progressive action. An alert
  2593. * message describes why an event occurred.
  2594. *
  2595. * @static
  2596. * @inheritable
  2597. * @property {jQuery|string|Function|null}
  2598. */
  2599. OO.ui.MessageDialog.static.message = null;
  2600. /**
  2601. * @static
  2602. * @inheritdoc
  2603. */
  2604. OO.ui.MessageDialog.static.actions = [
  2605. // Note that OO.ui.alert() and OO.ui.confirm() rely on these.
  2606. { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
  2607. { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
  2608. ];
  2609. /* Methods */
  2610. /**
  2611. * Toggle action layout between vertical and horizontal.
  2612. *
  2613. * @private
  2614. * @param {boolean} [value] Layout actions vertically, omit to toggle
  2615. * @chainable
  2616. */
  2617. OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
  2618. value = value === undefined ? !this.verticalActionLayout : !!value;
  2619. if ( value !== this.verticalActionLayout ) {
  2620. this.verticalActionLayout = value;
  2621. this.$actions
  2622. .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
  2623. .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
  2624. }
  2625. return this;
  2626. };
  2627. /**
  2628. * @inheritdoc
  2629. */
  2630. OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) {
  2631. if ( action ) {
  2632. return new OO.ui.Process( function () {
  2633. this.close( { action: action } );
  2634. }, this );
  2635. }
  2636. return OO.ui.MessageDialog.parent.prototype.getActionProcess.call( this, action );
  2637. };
  2638. /**
  2639. * @inheritdoc
  2640. *
  2641. * @param {Object} [data] Dialog opening data
  2642. * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
  2643. * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
  2644. * @param {string} [data.size] Symbolic name of the dialog size, see OO.ui.Window
  2645. * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
  2646. * action item
  2647. */
  2648. OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
  2649. data = data || {};
  2650. // Parent method
  2651. return OO.ui.MessageDialog.parent.prototype.getSetupProcess.call( this, data )
  2652. .next( function () {
  2653. this.title.setLabel(
  2654. data.title !== undefined ? data.title : this.constructor.static.title
  2655. );
  2656. this.message.setLabel(
  2657. data.message !== undefined ? data.message : this.constructor.static.message
  2658. );
  2659. this.size = data.size !== undefined ? data.size : this.constructor.static.size;
  2660. }, this );
  2661. };
  2662. /**
  2663. * @inheritdoc
  2664. */
  2665. OO.ui.MessageDialog.prototype.getReadyProcess = function ( data ) {
  2666. data = data || {};
  2667. // Parent method
  2668. return OO.ui.MessageDialog.parent.prototype.getReadyProcess.call( this, data )
  2669. .next( function () {
  2670. // Focus the primary action button
  2671. var actions = this.actions.get();
  2672. actions = actions.filter( function ( action ) {
  2673. return action.getFlags().indexOf( 'primary' ) > -1;
  2674. } );
  2675. if ( actions.length > 0 ) {
  2676. actions[ 0 ].focus();
  2677. }
  2678. }, this );
  2679. };
  2680. /**
  2681. * @inheritdoc
  2682. */
  2683. OO.ui.MessageDialog.prototype.getBodyHeight = function () {
  2684. var bodyHeight, oldOverflow,
  2685. $scrollable = this.container.$element;
  2686. oldOverflow = $scrollable[ 0 ].style.overflow;
  2687. $scrollable[ 0 ].style.overflow = 'hidden';
  2688. OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
  2689. bodyHeight = this.text.$element.outerHeight( true );
  2690. $scrollable[ 0 ].style.overflow = oldOverflow;
  2691. return bodyHeight;
  2692. };
  2693. /**
  2694. * @inheritdoc
  2695. */
  2696. OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
  2697. var
  2698. dialog = this,
  2699. $scrollable = this.container.$element;
  2700. OO.ui.MessageDialog.parent.prototype.setDimensions.call( this, dim );
  2701. // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
  2702. // Need to do it after transition completes (250ms), add 50ms just in case.
  2703. setTimeout( function () {
  2704. var oldOverflow = $scrollable[ 0 ].style.overflow,
  2705. activeElement = document.activeElement;
  2706. $scrollable[ 0 ].style.overflow = 'hidden';
  2707. OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
  2708. // Check reconsiderScrollbars didn't destroy our focus, as we
  2709. // are doing this after the ready process.
  2710. if ( activeElement && activeElement !== document.activeElement && activeElement.focus ) {
  2711. activeElement.focus();
  2712. }
  2713. $scrollable[ 0 ].style.overflow = oldOverflow;
  2714. }, 300 );
  2715. dialog.fitActions();
  2716. // Wait for CSS transition to finish and do it again :(
  2717. setTimeout( function () {
  2718. dialog.fitActions();
  2719. }, 300 );
  2720. return this;
  2721. };
  2722. /**
  2723. * @inheritdoc
  2724. */
  2725. OO.ui.MessageDialog.prototype.initialize = function () {
  2726. // Parent method
  2727. OO.ui.MessageDialog.parent.prototype.initialize.call( this );
  2728. // Properties
  2729. this.$actions = $( '<div>' );
  2730. this.container = new OO.ui.PanelLayout( {
  2731. scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
  2732. } );
  2733. this.text = new OO.ui.PanelLayout( {
  2734. padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
  2735. } );
  2736. this.message = new OO.ui.LabelWidget( {
  2737. classes: [ 'oo-ui-messageDialog-message' ]
  2738. } );
  2739. // Initialization
  2740. this.title.$element.addClass( 'oo-ui-messageDialog-title' );
  2741. this.$content.addClass( 'oo-ui-messageDialog-content' );
  2742. this.container.$element.append( this.text.$element );
  2743. this.text.$element.append( this.title.$element, this.message.$element );
  2744. this.$body.append( this.container.$element );
  2745. this.$actions.addClass( 'oo-ui-messageDialog-actions' );
  2746. this.$foot.append( this.$actions );
  2747. };
  2748. /**
  2749. * @inheritdoc
  2750. */
  2751. OO.ui.MessageDialog.prototype.getActionWidgetConfig = function ( config ) {
  2752. // Force unframed
  2753. return $.extend( {}, config, { framed: false } );
  2754. };
  2755. /**
  2756. * @inheritdoc
  2757. */
  2758. OO.ui.MessageDialog.prototype.attachActions = function () {
  2759. var i, len, special, others;
  2760. // Parent method
  2761. OO.ui.MessageDialog.parent.prototype.attachActions.call( this );
  2762. special = this.actions.getSpecial();
  2763. others = this.actions.getOthers();
  2764. if ( special.safe ) {
  2765. this.$actions.append( special.safe.$element );
  2766. special.safe.toggleFramed( true );
  2767. }
  2768. for ( i = 0, len = others.length; i < len; i++ ) {
  2769. this.$actions.append( others[ i ].$element );
  2770. others[ i ].toggleFramed( true );
  2771. }
  2772. if ( special.primary ) {
  2773. this.$actions.append( special.primary.$element );
  2774. special.primary.toggleFramed( true );
  2775. }
  2776. };
  2777. /**
  2778. * Fit action actions into columns or rows.
  2779. *
  2780. * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
  2781. *
  2782. * @private
  2783. */
  2784. OO.ui.MessageDialog.prototype.fitActions = function () {
  2785. var i, len, action,
  2786. previous = this.verticalActionLayout,
  2787. actions = this.actions.get();
  2788. // Detect clipping
  2789. this.toggleVerticalActionLayout( false );
  2790. for ( i = 0, len = actions.length; i < len; i++ ) {
  2791. action = actions[ i ];
  2792. if ( action.$element[ 0 ].scrollWidth > action.$element[ 0 ].clientWidth ) {
  2793. this.toggleVerticalActionLayout( true );
  2794. break;
  2795. }
  2796. }
  2797. // Move the body out of the way of the foot
  2798. this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
  2799. if ( this.verticalActionLayout !== previous ) {
  2800. // We changed the layout, window height might need to be updated.
  2801. this.updateSize();
  2802. }
  2803. };
  2804. /**
  2805. * ProcessDialog windows encapsulate a {@link OO.ui.Process process} and all of the code necessary
  2806. * to complete it. If the process terminates with an error, a customizable {@link OO.ui.Error error
  2807. * interface} alerts users to the trouble, permitting the user to dismiss the error and try again when
  2808. * relevant. The ProcessDialog class is always extended and customized with the actions and content
  2809. * required for each process.
  2810. *
  2811. * The process dialog box consists of a header that visually represents the ‘working’ state of long
  2812. * processes with an animation. The header contains the dialog title as well as
  2813. * two {@link OO.ui.ActionWidget action widgets}: a ‘safe’ action on the left (e.g., ‘Cancel’) and
  2814. * a ‘primary’ action on the right (e.g., ‘Done’).
  2815. *
  2816. * Like other windows, the process dialog is managed by a {@link OO.ui.WindowManager window manager}.
  2817. * Please see the [OOUI documentation on MediaWiki][1] for more information and examples.
  2818. *
  2819. * @example
  2820. * // Example: Creating and opening a process dialog window.
  2821. * function MyProcessDialog( config ) {
  2822. * MyProcessDialog.parent.call( this, config );
  2823. * }
  2824. * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
  2825. *
  2826. * MyProcessDialog.static.name = 'myProcessDialog';
  2827. * MyProcessDialog.static.title = 'Process dialog';
  2828. * MyProcessDialog.static.actions = [
  2829. * { action: 'save', label: 'Done', flags: 'primary' },
  2830. * { label: 'Cancel', flags: 'safe' }
  2831. * ];
  2832. *
  2833. * MyProcessDialog.prototype.initialize = function () {
  2834. * MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
  2835. * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
  2836. * this.content.$element.append( '<p>This is a process dialog window. The header contains the title and two buttons: \'Cancel\' (a safe action) on the left and \'Done\' (a primary action) on the right.</p>' );
  2837. * this.$body.append( this.content.$element );
  2838. * };
  2839. * MyProcessDialog.prototype.getActionProcess = function ( action ) {
  2840. * var dialog = this;
  2841. * if ( action ) {
  2842. * return new OO.ui.Process( function () {
  2843. * dialog.close( { action: action } );
  2844. * } );
  2845. * }
  2846. * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
  2847. * };
  2848. *
  2849. * var windowManager = new OO.ui.WindowManager();
  2850. * $( 'body' ).append( windowManager.$element );
  2851. *
  2852. * var dialog = new MyProcessDialog();
  2853. * windowManager.addWindows( [ dialog ] );
  2854. * windowManager.openWindow( dialog );
  2855. *
  2856. * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs
  2857. *
  2858. * @abstract
  2859. * @class
  2860. * @extends OO.ui.Dialog
  2861. *
  2862. * @constructor
  2863. * @param {Object} [config] Configuration options
  2864. */
  2865. OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
  2866. // Parent constructor
  2867. OO.ui.ProcessDialog.parent.call( this, config );
  2868. // Properties
  2869. this.fitOnOpen = false;
  2870. // Initialization
  2871. this.$element.addClass( 'oo-ui-processDialog' );
  2872. };
  2873. /* Setup */
  2874. OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
  2875. /* Methods */
  2876. /**
  2877. * Handle dismiss button click events.
  2878. *
  2879. * Hides errors.
  2880. *
  2881. * @private
  2882. */
  2883. OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
  2884. this.hideErrors();
  2885. };
  2886. /**
  2887. * Handle retry button click events.
  2888. *
  2889. * Hides errors and then tries again.
  2890. *
  2891. * @private
  2892. */
  2893. OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
  2894. this.hideErrors();
  2895. this.executeAction( this.currentAction );
  2896. };
  2897. /**
  2898. * @inheritdoc
  2899. */
  2900. OO.ui.ProcessDialog.prototype.initialize = function () {
  2901. // Parent method
  2902. OO.ui.ProcessDialog.parent.prototype.initialize.call( this );
  2903. // Properties
  2904. this.$navigation = $( '<div>' );
  2905. this.$location = $( '<div>' );
  2906. this.$safeActions = $( '<div>' );
  2907. this.$primaryActions = $( '<div>' );
  2908. this.$otherActions = $( '<div>' );
  2909. this.dismissButton = new OO.ui.ButtonWidget( {
  2910. label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
  2911. } );
  2912. this.retryButton = new OO.ui.ButtonWidget();
  2913. this.$errors = $( '<div>' );
  2914. this.$errorsTitle = $( '<div>' );
  2915. // Events
  2916. this.dismissButton.connect( this, { click: 'onDismissErrorButtonClick' } );
  2917. this.retryButton.connect( this, { click: 'onRetryButtonClick' } );
  2918. // Initialization
  2919. this.title.$element.addClass( 'oo-ui-processDialog-title' );
  2920. this.$location
  2921. .append( this.title.$element )
  2922. .addClass( 'oo-ui-processDialog-location' );
  2923. this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' );
  2924. this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' );
  2925. this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' );
  2926. this.$errorsTitle
  2927. .addClass( 'oo-ui-processDialog-errors-title' )
  2928. .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
  2929. this.$errors
  2930. .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
  2931. .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
  2932. this.$content
  2933. .addClass( 'oo-ui-processDialog-content' )
  2934. .append( this.$errors );
  2935. this.$navigation
  2936. .addClass( 'oo-ui-processDialog-navigation' )
  2937. // Note: Order of appends below is important. These are in the order
  2938. // we want tab to go through them. Display-order is handled entirely
  2939. // by CSS absolute-positioning. As such, primary actions like "done"
  2940. // should go first.
  2941. .append( this.$primaryActions, this.$location, this.$safeActions );
  2942. this.$head.append( this.$navigation );
  2943. this.$foot.append( this.$otherActions );
  2944. };
  2945. /**
  2946. * @inheritdoc
  2947. */
  2948. OO.ui.ProcessDialog.prototype.getActionWidgetConfig = function ( config ) {
  2949. var isMobile = OO.ui.isMobile();
  2950. // Default to unframed on mobile
  2951. config = $.extend( { framed: !isMobile }, config );
  2952. // Change back buttons to icon only on mobile
  2953. if (
  2954. isMobile &&
  2955. ( config.flags === 'back' || ( Array.isArray( config.flags ) && config.flags.indexOf( 'back' ) !== -1 ) )
  2956. ) {
  2957. $.extend( config, {
  2958. icon: 'previous',
  2959. label: ''
  2960. } );
  2961. }
  2962. return config;
  2963. };
  2964. /**
  2965. * @inheritdoc
  2966. */
  2967. OO.ui.ProcessDialog.prototype.attachActions = function () {
  2968. var i, len, other, special, others;
  2969. // Parent method
  2970. OO.ui.ProcessDialog.parent.prototype.attachActions.call( this );
  2971. special = this.actions.getSpecial();
  2972. others = this.actions.getOthers();
  2973. if ( special.primary ) {
  2974. this.$primaryActions.append( special.primary.$element );
  2975. }
  2976. for ( i = 0, len = others.length; i < len; i++ ) {
  2977. other = others[ i ];
  2978. this.$otherActions.append( other.$element );
  2979. }
  2980. if ( special.safe ) {
  2981. this.$safeActions.append( special.safe.$element );
  2982. }
  2983. };
  2984. /**
  2985. * @inheritdoc
  2986. */
  2987. OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
  2988. var process = this;
  2989. return OO.ui.ProcessDialog.parent.prototype.executeAction.call( this, action )
  2990. .fail( function ( errors ) {
  2991. process.showErrors( errors || [] );
  2992. } );
  2993. };
  2994. /**
  2995. * @inheritdoc
  2996. */
  2997. OO.ui.ProcessDialog.prototype.setDimensions = function () {
  2998. var dialog = this;
  2999. // Parent method
  3000. OO.ui.ProcessDialog.parent.prototype.setDimensions.apply( this, arguments );
  3001. this.fitLabel();
  3002. // If there are many actions, they might be shown on multiple lines. Their layout can change when
  3003. // resizing the dialog and when changing the actions. Adjust the height of the footer to fit them.
  3004. dialog.$body.css( 'bottom', dialog.$foot.outerHeight( true ) );
  3005. // Wait for CSS transition to finish and do it again :(
  3006. setTimeout( function () {
  3007. dialog.$body.css( 'bottom', dialog.$foot.outerHeight( true ) );
  3008. }, 300 );
  3009. };
  3010. /**
  3011. * Fit label between actions.
  3012. *
  3013. * @private
  3014. * @chainable
  3015. */
  3016. OO.ui.ProcessDialog.prototype.fitLabel = function () {
  3017. var safeWidth, primaryWidth, biggerWidth, labelWidth, navigationWidth, leftWidth, rightWidth,
  3018. size = this.getSizeProperties();
  3019. if ( typeof size.width !== 'number' ) {
  3020. if ( this.isOpened() ) {
  3021. navigationWidth = this.$head.width() - 20;
  3022. } else if ( this.isOpening() ) {
  3023. if ( !this.fitOnOpen ) {
  3024. // Size is relative and the dialog isn't open yet, so wait.
  3025. // FIXME: This should ideally be handled by setup somehow.
  3026. this.manager.lifecycle.opened.done( this.fitLabel.bind( this ) );
  3027. this.fitOnOpen = true;
  3028. }
  3029. return;
  3030. } else {
  3031. return;
  3032. }
  3033. } else {
  3034. navigationWidth = size.width - 20;
  3035. }
  3036. safeWidth = this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0;
  3037. primaryWidth = this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0;
  3038. biggerWidth = Math.max( safeWidth, primaryWidth );
  3039. labelWidth = this.title.$element.width();
  3040. if ( 2 * biggerWidth + labelWidth < navigationWidth ) {
  3041. // We have enough space to center the label
  3042. leftWidth = rightWidth = biggerWidth;
  3043. } else {
  3044. // Let's hope we at least have enough space not to overlap, because we can't wrap the label…
  3045. if ( this.getDir() === 'ltr' ) {
  3046. leftWidth = safeWidth;
  3047. rightWidth = primaryWidth;
  3048. } else {
  3049. leftWidth = primaryWidth;
  3050. rightWidth = safeWidth;
  3051. }
  3052. }
  3053. this.$location.css( { paddingLeft: leftWidth, paddingRight: rightWidth } );
  3054. return this;
  3055. };
  3056. /**
  3057. * Handle errors that occurred during accept or reject processes.
  3058. *
  3059. * @private
  3060. * @param {OO.ui.Error[]|OO.ui.Error} errors Errors to be handled
  3061. */
  3062. OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
  3063. var i, len, $item, actions,
  3064. items = [],
  3065. abilities = {},
  3066. recoverable = true,
  3067. warning = false;
  3068. if ( errors instanceof OO.ui.Error ) {
  3069. errors = [ errors ];
  3070. }
  3071. for ( i = 0, len = errors.length; i < len; i++ ) {
  3072. if ( !errors[ i ].isRecoverable() ) {
  3073. recoverable = false;
  3074. }
  3075. if ( errors[ i ].isWarning() ) {
  3076. warning = true;
  3077. }
  3078. $item = $( '<div>' )
  3079. .addClass( 'oo-ui-processDialog-error' )
  3080. .append( errors[ i ].getMessage() );
  3081. items.push( $item[ 0 ] );
  3082. }
  3083. this.$errorItems = $( items );
  3084. if ( recoverable ) {
  3085. abilities[ this.currentAction ] = true;
  3086. // Copy the flags from the first matching action
  3087. actions = this.actions.get( { actions: this.currentAction } );
  3088. if ( actions.length ) {
  3089. this.retryButton.clearFlags().setFlags( actions[ 0 ].getFlags() );
  3090. }
  3091. } else {
  3092. abilities[ this.currentAction ] = false;
  3093. this.actions.setAbilities( abilities );
  3094. }
  3095. if ( warning ) {
  3096. this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) );
  3097. } else {
  3098. this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) );
  3099. }
  3100. this.retryButton.toggle( recoverable );
  3101. this.$errorsTitle.after( this.$errorItems );
  3102. this.$errors.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
  3103. };
  3104. /**
  3105. * Hide errors.
  3106. *
  3107. * @private
  3108. */
  3109. OO.ui.ProcessDialog.prototype.hideErrors = function () {
  3110. this.$errors.addClass( 'oo-ui-element-hidden' );
  3111. if ( this.$errorItems ) {
  3112. this.$errorItems.remove();
  3113. this.$errorItems = null;
  3114. }
  3115. };
  3116. /**
  3117. * @inheritdoc
  3118. */
  3119. OO.ui.ProcessDialog.prototype.getTeardownProcess = function ( data ) {
  3120. // Parent method
  3121. return OO.ui.ProcessDialog.parent.prototype.getTeardownProcess.call( this, data )
  3122. .first( function () {
  3123. // Make sure to hide errors
  3124. this.hideErrors();
  3125. this.fitOnOpen = false;
  3126. }, this );
  3127. };
  3128. /**
  3129. * @class OO.ui
  3130. */
  3131. /**
  3132. * Lazy-initialize and return a global OO.ui.WindowManager instance, used by OO.ui.alert and
  3133. * OO.ui.confirm.
  3134. *
  3135. * @private
  3136. * @return {OO.ui.WindowManager}
  3137. */
  3138. OO.ui.getWindowManager = function () {
  3139. if ( !OO.ui.windowManager ) {
  3140. OO.ui.windowManager = new OO.ui.WindowManager();
  3141. $( 'body' ).append( OO.ui.windowManager.$element );
  3142. OO.ui.windowManager.addWindows( [ new OO.ui.MessageDialog() ] );
  3143. }
  3144. return OO.ui.windowManager;
  3145. };
  3146. /**
  3147. * Display a quick modal alert dialog, using a OO.ui.MessageDialog. While the dialog is open, the
  3148. * rest of the page will be dimmed out and the user won't be able to interact with it. The dialog
  3149. * has only one action button, labelled "OK", clicking it will simply close the dialog.
  3150. *
  3151. * A window manager is created automatically when this function is called for the first time.
  3152. *
  3153. * @example
  3154. * OO.ui.alert( 'Something happened!' ).done( function () {
  3155. * console.log( 'User closed the dialog.' );
  3156. * } );
  3157. *
  3158. * OO.ui.alert( 'Something larger happened!', { size: 'large' } );
  3159. *
  3160. * @param {jQuery|string} text Message text to display
  3161. * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
  3162. * @return {jQuery.Promise} Promise resolved when the user closes the dialog
  3163. */
  3164. OO.ui.alert = function ( text, options ) {
  3165. return OO.ui.getWindowManager().openWindow( 'message', $.extend( {
  3166. message: text,
  3167. actions: [ OO.ui.MessageDialog.static.actions[ 0 ] ]
  3168. }, options ) ).closed.then( function () {
  3169. return undefined;
  3170. } );
  3171. };
  3172. /**
  3173. * Display a quick modal confirmation dialog, using a OO.ui.MessageDialog. While the dialog is open,
  3174. * the rest of the page will be dimmed out and the user won't be able to interact with it. The
  3175. * dialog has two action buttons, one to confirm an operation (labelled "OK") and one to cancel it
  3176. * (labelled "Cancel").
  3177. *
  3178. * A window manager is created automatically when this function is called for the first time.
  3179. *
  3180. * @example
  3181. * OO.ui.confirm( 'Are you sure?' ).done( function ( confirmed ) {
  3182. * if ( confirmed ) {
  3183. * console.log( 'User clicked "OK"!' );
  3184. * } else {
  3185. * console.log( 'User clicked "Cancel" or closed the dialog.' );
  3186. * }
  3187. * } );
  3188. *
  3189. * @param {jQuery|string} text Message text to display
  3190. * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
  3191. * @return {jQuery.Promise} Promise resolved when the user closes the dialog. If the user chose to
  3192. * confirm, the promise will resolve to boolean `true`; otherwise, it will resolve to boolean
  3193. * `false`.
  3194. */
  3195. OO.ui.confirm = function ( text, options ) {
  3196. return OO.ui.getWindowManager().openWindow( 'message', $.extend( {
  3197. message: text
  3198. }, options ) ).closed.then( function ( data ) {
  3199. return !!( data && data.action === 'accept' );
  3200. } );
  3201. };
  3202. /**
  3203. * Display a quick modal prompt dialog, using a OO.ui.MessageDialog. While the dialog is open,
  3204. * the rest of the page will be dimmed out and the user won't be able to interact with it. The
  3205. * dialog has a text input widget and two action buttons, one to confirm an operation (labelled "OK")
  3206. * and one to cancel it (labelled "Cancel").
  3207. *
  3208. * A window manager is created automatically when this function is called for the first time.
  3209. *
  3210. * @example
  3211. * OO.ui.prompt( 'Choose a line to go to', { textInput: { placeholder: 'Line number' } } ).done( function ( result ) {
  3212. * if ( result !== null ) {
  3213. * console.log( 'User typed "' + result + '" then clicked "OK".' );
  3214. * } else {
  3215. * console.log( 'User clicked "Cancel" or closed the dialog.' );
  3216. * }
  3217. * } );
  3218. *
  3219. * @param {jQuery|string} text Message text to display
  3220. * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
  3221. * @param {Object} [options.textInput] Additional options for text input widget, see OO.ui.TextInputWidget
  3222. * @return {jQuery.Promise} Promise resolved when the user closes the dialog. If the user chose to
  3223. * confirm, the promise will resolve with the value of the text input widget; otherwise, it will
  3224. * resolve to `null`.
  3225. */
  3226. OO.ui.prompt = function ( text, options ) {
  3227. var instance,
  3228. manager = OO.ui.getWindowManager(),
  3229. textInput = new OO.ui.TextInputWidget( ( options && options.textInput ) || {} ),
  3230. textField = new OO.ui.FieldLayout( textInput, {
  3231. align: 'top',
  3232. label: text
  3233. } );
  3234. instance = manager.openWindow( 'message', $.extend( {
  3235. message: textField.$element
  3236. }, options ) );
  3237. // TODO: This is a little hacky, and could be done by extending MessageDialog instead.
  3238. instance.opened.then( function () {
  3239. textInput.on( 'enter', function () {
  3240. manager.getCurrentWindow().close( { action: 'accept' } );
  3241. } );
  3242. textInput.focus();
  3243. } );
  3244. return instance.closed.then( function ( data ) {
  3245. return data && data.action === 'accept' ? textInput.getValue() : null;
  3246. } );
  3247. };
  3248. }( OO ) );
  3249. //# sourceMappingURL=oojs-ui-windows.js.map.json