8 v-model:selected="selection"
9 v-model:input-value="currentSearchTerm"
10 class="mw-block-target"
14 :menu-items="menuItems"
15 :placeholder="$i18n( 'block-target-placeholder' ).text()"
16 :start-icon="cdxIconSearch"
21 @update:selected="onSelect"
25 {{ $i18n( 'block-target' ).text() }}
27 <div class="mw-block-conveniencelinks">
28 <span v-if="!!targetUser">
30 :href="mw.util.getUrl( contribsTitle )"
31 :title="contribsTitle"
33 {{ $i18n( 'ipb-blocklist-contribs', targetUser ) }}
41 const { computed, defineComponent, onMounted, ref, watch } = require( 'vue' );
42 const { CdxLookup, CdxField } = require( '@wikimedia/codex' );
43 const { storeToRefs } = require( 'pinia' );
44 const { cdxIconSearch } = require( '../icons.json' );
45 const useBlockStore = require( '../stores/block.js' );
46 const api = new mw.Api();
49 * User lookup component for Special:Block.
51 * @todo Abstract for general use in MediaWiki (T375220)
53 module.exports = exports = defineComponent( {
55 components: { CdxLookup, CdxField },
57 modelValue: { type: [ String, null ], required: true }
63 const store = useBlockStore();
64 const { targetUser } = storeToRefs( store );
68 // Get the input element.
69 htmlInput = document.querySelector( 'input[name="wpTarget"]' );
70 // Focus the input on mount.
74 // Set a flag to keep track of pending API requests, so we can abort if
75 // the target string changes
78 // Codex Lookup component requires a v-modeled `selected` prop.
79 // Until a selection is made, the value may be set to null.
80 // We instead want to only update the targetUser for non-null values
81 // (made either via selection, or the 'change' event).
82 const selection = ref( props.modelValue || '' );
83 // This is the source of truth for what should be the target user,
84 // but it should only change on 'change' or 'select' events,
85 // otherwise we'd fire off API queries for the block log unnecessarily.
86 const currentSearchTerm = ref( props.modelValue || '' );
87 const menuItems = ref( [] );
88 const status = ref( 'default' );
89 const messages = ref( {} );
91 watch( targetUser, ( newValue ) => {
93 currentSearchTerm.value = newValue;
100 * @param {string} searchTerm
103 function fetchResults( searchTerm ) {
113 return api.get( params )
114 .then( ( response ) => response.query );
118 * Handle lookup input.
120 * @param {string} value
122 function onInput( value ) {
123 // Abort any existing request if one is still pending
129 // Internally track the current search term.
130 currentSearchTerm.value = value;
132 // Do nothing if we have no input.
134 menuItems.value = [];
138 fetchResults( value )
142 // Make sure this data is still relevant first.
143 if ( currentSearchTerm.value !== value ) {
147 // Reset the menu items if there are no results.
148 if ( !data.allusers || data.allusers.length === 0 ) {
149 menuItems.value = [];
153 // Build an array of menu items.
154 menuItems.value = data.allusers.map( ( result ) => ( {
160 // On error, set results to empty.
161 menuItems.value = [];
166 * Validate the input element.
168 * @param {HTMLInputElement} el
170 function validate( el ) {
171 if ( el.checkValidity() ) {
172 status.value = 'default';
175 status.value = 'error';
176 messages.value = { error: el.validationMessage };
181 * Handle lookup change.
183 function onChange() {
184 // Use the currentSearchTerm value instead of the event target value,
185 // since the event can be fired before the watcher updates the value.
186 setTarget( currentSearchTerm.value );
190 * When the clear button is clicked.
193 store.resetForm( true );
198 * Handle lookup selection.
200 function onSelect() {
201 if ( selection.value !== null ) {
202 setTarget( selection.value );
207 * Set the target user and trigger validation.
209 * @param {string} value
211 function setTarget( value ) {
212 validate( htmlInput );
213 targetUser.value = value;
216 // Change the address bar to reflect the newly-selected target (while keeping all URL parameters).
217 // Do this when the targetUser changes, which is not necessarily when the CdxLookup selection changes.
218 watch( () => targetUser.value, () => {
219 const specialBlockUrl = mw.util.getUrl( 'Special:Block' + ( targetUser.value ? '/' + targetUser.value : '' ) );
220 if ( window.location.pathname !== specialBlockUrl ) {
221 const newUrl = ( new URL( `${ specialBlockUrl }${ window.location.search }`, window.location.origin ) ).toString();
222 window.history.replaceState( null, '', newUrl );
226 const contribsTitle = computed( () => `Special:Contributions/${ targetUser.value }` );
248 .mw-block-conveniencelinks {