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.
importArticle({
type: 'script',
article: 'u:dev:MediaWiki:ShowCustomModal.js'
});
mw.hook('dev.showCustomModal').add(function(showCustomModal) {
// Your code here
// `showCustomModal` is a binding to `window.dev.showCustomModal`
});
<documentation>
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()
ormw.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, aftermessage
.
- 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 theshowCustomModal
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 Esc 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 withcloseModal
. The default value istrue
. - 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 ofcloseModal
internally, keeping the modal contents around for showing later withshowModal
. 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.
Examples
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:
dev.showCustomModal('Clickity', {
content: 'Click the button to show a second modal!',
buttons: [
{
message: 'Modalception',
defaultButton: true,
handler: function() {
dev.showCustomModal('You did it!', 'Good job.');
}
}
]
});
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:
dev.showCustomModal("You're trapped!", {
content: 'Mwahaha',
onClose: function() {
// No escape!
return false;
}
});
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:
dev.showCustomModal("You can only close when the minutes are even", {
onClose: function() {
return new Date().getMinutes() % 2 === 0;
}
});
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!
var $modal = dev.showCustomModal('Close prompt', {
onClose: function() {
var $confirmation = dev.showCustomModal('Are you sure you want to close?', {
buttons: [
{
message: 'No!',
defaultButton: true,
handler: function() {
dev.showCustomModal.closeModal($confirmation);
}
},
{
message: 'Yes!',
handler: function() {
dev.showCustomModal.closeModal($confirmation);
dev.showCustomModal.closeModal($modal);
}
}
]
});
// We always return false, showing a modal instead; using the manual showCustomModal.closeModal doesn't call this function!
return false;
}
});
This example has way more moving parts, so let's analyze it:
- The modal has an
onClose
property, with a function that always returnsfalse
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:
var $modal = dev.showCustomModal(i18n.msg('my-modal-title').escape(), {
content: ui.div({
id: 'my-modal-wrapper',
children: [
// Using .plain() as Dorui escapes strings of HTML by default
i18n.msg('my-modal-description').plain(),
ui.div({
id: 'random-number-container',
text: Math.random()
})
]
}),
buttons: [
{
message: i18n.msg('my-modal-button-randomize').escape(),
defaultButton: true,
handler: function() {
$modal.find('#random-number-container').text(Math.random());
}
},
{
message: i18n.msg('my-modal-button-close').escape(),
handler: function() {
dev.showCustomModal.closeModal($modal);
}
}
]
});
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
<div>
element with anid="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.