ShowCustomModal

ShowCustomModal is a JavaScript library that duplicates the functionality for the now-defunct $.showCustomModal.
It is no longer exposed through the global jQuery object, but is instead exposed through a mw hook. That hook is fired with a function that would correspond to $.showCustomModal, and it also contains the other relevant methods to modal manipulation, such as closeModal.
Its existence is rooted in two factors: Legacy scripts exist that require modal support, alternatives being Modal-js and QDmodal. Porting to them is non-trivial. This exists to ease that transition. Another reason for it to exist is archival reasons, along with providing an alternative to both Modal-js's (mostly OOUI-rooted) and QDmodal's shortcomings, and a simpler stateless modal function call.
== Importing ==
You know how this goes.
== ==
The hook exports a single function, the showCustomModal function. It is not a constructor, you don't call it with new; you call it to show a modal, and it returns the constructed modal jQuery object.
If you've used $.showCustomModal before, it's completely backwards compatible. What this means is that any call to $.showCustomModal you can find can be replaced with dev.showCustomModal and it will "Just Work", as long as you make sure the hook is fired before using it.
Functions in JavaScript are also objects, which means a function can have properties. The exported showCustomModal function has some of these too, for example, closeModal. If you wanted to use it, you would grab your reference to the jQuery object showCustomModal returned, and call it: showCustomModal.closeModal($modal)
; showCustomModal(title, content?, options?)
: The main function. The first parameter is a string, and it's the modal's title. It's interpreted as HTML, so you have to sanitize it. This bears repeating: both title and content are interpreted as HTML, if you don't use measures like mw.html.escape, your script will be vulnerable.
: With that out of the way, the second parameter is the content, and the third is an options object with many properties you can use. One of them is also content, so you can avoid passing the second attribute and replace it with an options object. This usage is also valid: showCustomModal(title, { content: string, ...otherOptions })
: The options object has many useful properties, that will be properly documented later on.
; showCustomModal.closeModal($modal)
: The second exported function, and most likely the most useful of the rather-niche extra functions provided.
: Given a jQuery instance referencing the .modalWrapper element(s), it will close them and get rid of their backdrops.
: There is a bug, where if you have multiple modal instances and close one of them, the body class .modalShown class will be removed, even if there is one modal still shown. If you really have a use case for this class, leave a message on the talk page. For now it's advised you simply not use the class.
; showCustomModal.hideModal($modal) & showCustomModal.showModal($modal)
: These functions simply hide and show modals, they're used internally when you specify the options persistent property. These have never been used in any dev script to my knowledge, so you probably don't need these, or the above property. Nevertheless, the functionality is here if you need a modal that you want to conserve its state after you close it, in case you wish to allow the user to take a peek to the backdrop before re-opening a modal.
; showCustomModal.makeModal($dialog, options) & showCustomModal.getModalTopOffset($modal)
: These are implementation detail functions. Don't use them. Or I will find you, and you don't want to see me in a bad mood. Madmin engaged.
=== The Options object ===
The third (or second, if content is omitted) parameter to showCustomModal. It's a plain object with many useful properties you may want to apply to your modal. It's where a lot of the complexity lays, and where the "custom" from "showCustomModal" comes from.
Here are all the available properties, ordered in a roughly most-to-least useful, while also giving a boost to simple but effective properties like id and width you should know about. The bottom half is likely broken, useless, and prone to being removed if not for compatibility reasons.
; content
: You should only use this if you use the signature for showCustomModal(title, options). It's provided as a stylistic choice to keep the function call simpler, even if the old signature is available for backwards compatibility.
; buttons
: The most common and useful property in the options object. It lets you define an array of buttons that will be shown at the bottom of the modal. Its signature is [{ defaultButton: boolean, message: string, id: string, handler: function(event) }].
:: defaultButton
:: A boolean that states whether the button should be marked as primary or not. Primary buttons will be darker, while secondary ones will usually stand out less.
:: message
:: The content for the button. This is HTML, so all the previous warnings about sanitization still apply. Use functions such as I18n-js's .escape() or mw.html.escape() to stay free of STDs.
:: id
:: The id attribute that will be assigned to the button. Usually, you shouldn't worry or care about this property. It could come in handy if you wish to disable some buttons programmatically, though, and a bug was fixed where buttons don't have an id="undefined" if you don't provide one.
:: handler
:: A function that will be executed when you click the modal. For example, a close button could call a closure that calls showCustomModal.closeModal. It's probably the second most useful parameter, after message.
; width
: A number of pixels that decides how wide the modal will be, or a string with a CSS unit of size. Default: 400, examples: 420, '50%', '100vw'
; height
: How tall the modal will be, just like width. You should probably leave this to its default value. Default: auto
; id
: An id to give the modal wrapper element. If omitted, it will be randomly assigned.
; className
: A class to add to the modal wrapper, on top of modalWrapper.
; callbackBefore, callback
: Functions that will be called before and after the modal is created, respectively. callback is not very useful as you can simply place the contents after the showCustomModal call, and reference the $modal object it returns. Still, callbackBefore is not very useful either, so my advice is to simply stay clear of these parameters whenever possible.
; onClose
: A very useful function you may need. It handles the modal closing, and if the function returns false, the modal will not be closed. If your program is currently running and you wish to prevent the user from accidentally closing the modal meanwhile, you have to use this. Leverage it wisely.
; onCreate
: A comparatively very useless function. It's called with a reference to the wrapping dialog (which is discarded), and to the modal wrapper. It's like callback, but has one extra parameter that serves no purpose. Don't use either of these.
; escapeToClose
: Whether or not to close the modal when the key is pressed. A boolean. Don't override unless you have a good reason to, like to prevent accidental modal closes. However, even if so, you make use of an onClose handler that returns false, and will stop the modal from closing unless you do it with closeModal. The default value is true.
; showCloseButton
: A boolean that decides whether or not the top-right corner "X" button is shown.
; blackoutOpacity
: The opacity to give the backdrop of the modal. It's a number between 0 and 1. Default: 0.65
; tabsOutsideContent
: Legacy setting for a modal-tabs component, you should find no use to this. The library provides no functionality to build these tab components.
; topOffset
: The top offset in pixels that the modal will be from the top of the screen. Has to be a number. You should probably leave this to its default value. Default: 50
; zIndex
: To override the default z-index. You shouldn't override this without a good reason.
; noHeadline
: A boolean that moves the modal title element to be above the close button, if it exists. This could be removed in the future, it's ugly and silly.
; suppressDefaultStyles
: It's in the name, removes most default CSS properties given to the modal. You will have to position it manually.
; persistent
: A boolean for whether hideModal should be used instead of closeModal internally, keeping the modal contents around for showing later with showModal. Don't expect users to understand this behavior without explanation, they will likely expect progress to be lost if the modal is closed.
; resizeModal
: Whether to automatically handle modal resizing. This likely has no effect. Don't use it.
== ==
=== Basic usage ===
You'll import the script and register a hook that, once the script has loaded, will immediately show a modal.
importArticle({
 type: 'script',
 article: 'u:dev:MediaWiki:ShowCustomModal.js'
});
mw.hook('dev.showCustomModal').add(function() {
 dev.showCustomModal('Hello, world!');
});
In the example above, the parameter to the mw.hook callback function is ignored. You could use it to invoke the custom modal function, but using dev.showCustomModal is easier in most cases.
=== Using buttons and content ===
From now on, all examples are assuming that you're running code inside or after the hook has fired.
In order to have buttons, you need to use the second argument to the showCustomModal function, and provide the buttons array:
As you can see from the example above, you can show modals on top of modals. Modal can't do this, OOUI doesn't let it.
You can also see that the second call to showCustomModal uses a string as the second parameter. This is because you can provide the content as the second argument to the function, and then an optional options object as the third.
This isn't recommended, though, and you should ideally always use an object for the second argument whenever possible. It's offered for backwards compatibility reasons, and as a convenience for very simple cases.
=== Manual handling of modals closing ===
This section will explain a few aspects regarding modals closing in ShowCustomModal.
showCloseButton is an options property that controls whether the "X" button on the top left should be shown.
escapeToClose is a similar property that decides whether the escape key should by close the modal.
You can't toggle whether clicking the backdrop closes the modal (it always does), but you can define your own onClose function that returns false.
For example, if you wanted to show a modal that can't be closed (don't do this!), trapping the user, you can do this:
Note that this example still has an "X" button, but clicking it will fire the onClose handler and render it useless.
This is pretty pointless, however this could be useful if you want to control when the modal is able to close!
For example, if you want to disable closing the modal while a certain condition is met:
However, if we hadn't told the user exactly when the modal is closable, it would be a pretty bad user experience.
You can show a second modal for the user to confirm that, indeed, they want to close the modal. This is what MassCategorization does if the user tries to close the modal while it's running, so the user doesn't lose progress!
This example has way more moving parts, so let's analyze it:
# The modal has an onClose property, with a function that always returns false and shows a prompt modal
# The prompt modal has two buttons: Yes and no
# No closes the confirmation modal, leaving the original modal shown
# Yes closes both the confirmation modal and the original
But wait! Our original modal has an onClose function that always returns false! Wouldn't it show another modal?
Good catch! showCustomModal.closeModal completely bypasses the onClose handler, so you don't have to worry about this.
The above is quite an useful pattern you may wish to adopt if your modals have potentially sensitive information you don't want the user to accidentally lose.
You may also wish to hide the top-right "X" button while the modal shouldn't be closed. There's no API for this, but some CSS or DOM manipulation should get you pretty far.
=== Usage with i18n-js ===
I18n-js is the de-facto standard for embedding translations in your scripts, it allows many facilities for parameters and formats for your messages.
Particularly, since ShowCustomModal uses HTML for most of its string inputs, it's useful for its HTML escaping functionality:
dev.showCustomModal(i18n.msg('modal-title').escape(), {
 content: i18n.msg('modal-contents').escape(),
 buttons: [
 {
 message: i18n.msg('modal-button').escape()
 }
 ]
});
Keep in mind that in the above code you must have already gotten an i18n instance with your script's translations.
This is beyond the scope of this example, but you can look into I18n-js for an example usage, or look at a more complex program with sophisticated dependency management.
=== Usage with Dorui ===
If you predict your modal's contents will be complicated, you may want to use a UI library.
jQuery is so out of fashion, so let's use Dorui alongside i18n-js, for a realistic-ish example:
Whew! That's a lot of code, hopefully by reading it once or twice you can tell apart what's going on.
Still lost? Here's an explanation:
# It's creating a modal with the title of the i18n message my-modal-title, some contents, and some buttons
# It assigns the modal to the variable $modal
# The content is a element with an id="my-modal-wrapper"
# Its children are two elements: One text node with a description of what the modal is (something about showing a random number, and refreshing it with one of the buttons), and a random number container
# That random number container is created with some random text initially with Math.random()
# There's a randomize button that finds the random number container inside the modal and sets its text again to Math.random()
# There's a close button that closes the modal with the reference we made before of $modal
Got it? Yes? No? Awesome.
=== Porting from $.showCustomModal ===
If you want to port a legacy script to use ShowCustomModal, you'll need to import this library, listen to the hook, and replace references to $.showCustomModal to the exported dev.showCustomModal function. Here are some examples of people doing the migration:
# Discord: here by me
# I18nEdit: here, also by me
# Medals: here by HumansCanWinElves
# FAQ: here by Agent Zuri
=== Porting from Modal-js ===
If you're coming from OOUI modals, you may want to adopt ShowCustomModal regardless, as it doesn't impose a height limitation and reflows are automatic.
Each invocation to the showCustomModal function creates its own instance, so you don't have to refresh the content, events, and buttons if they're dynamic on each show of the modal.
There haven't been any ports from Modal-js to ShowCustomModal yet, but MassCategorization is a potential target due to its highly dynamic content size by nature.