Skip to content

Commit 9951b1e

Browse files
authored
Merge pull request #194 from Automattic/feature/42-conditional-autocomplete
2 parents 87cc1b8 + 62e46d4 commit 9951b1e

File tree

8 files changed

+1054
-53
lines changed

8 files changed

+1054
-53
lines changed

acm-autocomplete.js

Lines changed: 394 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,394 @@
1+
/**
2+
* Conditional Autocomplete for Ad Code Manager
3+
*
4+
* Provides Select2-powered autocomplete for conditional arguments
5+
* like categories, tags, pages, etc.
6+
*
7+
* @since 0.9.0
8+
*/
9+
( function( $, acmAutocomplete ) {
10+
'use strict';
11+
12+
if ( typeof acmAutocomplete === 'undefined' ) {
13+
return;
14+
}
15+
16+
var ConditionalAutocomplete = {
17+
18+
/**
19+
* Check if Select2 is available.
20+
*
21+
* @return {boolean} True if Select2 is loaded.
22+
*/
23+
isSelect2Available: function() {
24+
return typeof $.fn.select2 === 'function';
25+
},
26+
27+
/**
28+
* Initialize the autocomplete functionality.
29+
*/
30+
init: function() {
31+
this.bindEvents();
32+
this.initExistingFields();
33+
this.updateAddButtonVisibility();
34+
},
35+
36+
/**
37+
* Bind event handlers.
38+
*/
39+
bindEvents: function() {
40+
var self = this;
41+
42+
// Use event delegation for dynamically added conditional selects (Add form only).
43+
$( document ).on( 'change', '#add-adcode select[name="acm-conditionals[]"]', function() {
44+
self.handleConditionalChange( $( this ) );
45+
self.updateAddButtonVisibility();
46+
});
47+
48+
// Re-initialize when new conditional rows are added.
49+
$( document ).on( 'click', '.add-more-conditionals', function() {
50+
// Small delay to allow DOM to update.
51+
setTimeout( function() {
52+
self.cleanupNewRows();
53+
self.updateAddButtonVisibility();
54+
}, 100 );
55+
});
56+
57+
// Handle remove conditional click.
58+
$( document ).on( 'click', '.acm-remove-conditional', function() {
59+
setTimeout( function() {
60+
self.updateAddButtonVisibility();
61+
}, 100 );
62+
});
63+
},
64+
65+
/**
66+
* Initialize any existing conditional fields on page load.
67+
* Only targets the Add form, not inline edit.
68+
*/
69+
initExistingFields: function() {
70+
var self = this;
71+
72+
// Only target the Add form to avoid conflicts with inline edit.
73+
$( '#add-adcode select[name="acm-conditionals[]"]' ).each( function() {
74+
var $select = $( this );
75+
var conditional = $select.val();
76+
var $argumentsContainer = $select.closest( '.conditional-single-field' ).find( '.conditional-arguments' );
77+
78+
// Hide arguments for empty selection or no-parameter conditionals.
79+
if ( ! conditional || self.hasNoParameters( conditional ) ) {
80+
$argumentsContainer.hide();
81+
return;
82+
}
83+
84+
// Show arguments and init autocomplete if applicable.
85+
$argumentsContainer.show();
86+
87+
// Only init autocomplete if not already initialized.
88+
var $existingSelect2 = $argumentsContainer.find( 'select.acm-autocomplete-select' );
89+
if ( self.hasAutocomplete( conditional ) && ! $existingSelect2.length ) {
90+
self.initAutocomplete( $select );
91+
}
92+
});
93+
},
94+
95+
/**
96+
* Clean up newly added conditional rows.
97+
*
98+
* When rows are cloned from the master template, they may contain
99+
* leftover Select2 markup that needs to be cleaned up.
100+
*/
101+
cleanupNewRows: function() {
102+
var self = this;
103+
104+
$( 'select[name="acm-conditionals[]"]' ).each( function() {
105+
var $conditionalSelect = $( this );
106+
var conditional = $conditionalSelect.val();
107+
var $argumentsContainer = $conditionalSelect.closest( '.conditional-single-field' ).find( '.conditional-arguments' );
108+
109+
// If no conditional is selected, clean up any Select2 remnants.
110+
if ( ! conditional ) {
111+
// Remove any cloned Select2 elements.
112+
$argumentsContainer.find( 'select.acm-autocomplete-select' ).remove();
113+
$argumentsContainer.find( '.select2-container' ).remove();
114+
115+
// Restore the original input if it was hidden.
116+
var $hiddenInput = $argumentsContainer.find( 'input[data-original-name="acm-arguments[]"]' );
117+
if ( $hiddenInput.length ) {
118+
$hiddenInput
119+
.attr( 'name', 'acm-arguments[]' )
120+
.removeAttr( 'data-original-name' )
121+
.val( '' )
122+
.show();
123+
}
124+
125+
// Ensure input exists and is visible with correct attributes.
126+
var $input = $argumentsContainer.find( 'input[name="acm-arguments[]"]' );
127+
if ( ! $input.length ) {
128+
$argumentsContainer.prepend( '<input name="acm-arguments[]" type="text" value="" size="20" />' );
129+
} else {
130+
$input.val( '' ).attr( 'type', 'text' ).show();
131+
}
132+
133+
// Hide the arguments container since no conditional is selected.
134+
$argumentsContainer.hide();
135+
}
136+
});
137+
},
138+
139+
/**
140+
* Handle when a conditional select changes.
141+
*
142+
* @param {jQuery} $select The conditional select element.
143+
*/
144+
handleConditionalChange: function( $select ) {
145+
var conditional = $select.val();
146+
var $argumentsContainer = $select.closest( '.conditional-single-field' ).find( '.conditional-arguments' );
147+
148+
// Check if we have a Select2 select element or the original input.
149+
var $existingSelect = $argumentsContainer.find( 'select.acm-autocomplete-select' );
150+
var $hiddenInput = $argumentsContainer.find( 'input[data-original-name="acm-arguments[]"]' );
151+
152+
// If there's a Select2 select, destroy it and restore the input.
153+
if ( $existingSelect.length ) {
154+
var currentVal = $existingSelect.val();
155+
$existingSelect.select2( 'destroy' );
156+
$existingSelect.remove();
157+
158+
// Restore the original input's name and visibility.
159+
$hiddenInput
160+
.attr( 'name', 'acm-arguments[]' )
161+
.removeAttr( 'data-original-name' )
162+
.val( currentVal || '' )
163+
.show();
164+
}
165+
166+
// Get the input (either restored or original).
167+
var $input = $argumentsContainer.find( 'input[name="acm-arguments[]"]' );
168+
169+
// Reset the input value.
170+
$input.val( '' );
171+
172+
// Hide arguments container when no conditional selected or for conditionals that take no parameters.
173+
if ( ! conditional || this.hasNoParameters( conditional ) ) {
174+
$argumentsContainer.hide();
175+
return;
176+
}
177+
178+
// Show arguments container for conditionals that take parameters.
179+
$argumentsContainer.show();
180+
181+
// If this conditional supports autocomplete, initialize it.
182+
if ( conditional && this.hasAutocomplete( conditional ) ) {
183+
this.initAutocomplete( $select );
184+
}
185+
},
186+
187+
/**
188+
* Check if a conditional has autocomplete configuration.
189+
*
190+
* @param {string} conditional The conditional function name.
191+
* @return {boolean} True if autocomplete is available.
192+
*/
193+
hasAutocomplete: function( conditional ) {
194+
return acmAutocomplete.conditionals.hasOwnProperty( conditional );
195+
},
196+
197+
/**
198+
* Get the autocomplete configuration for a conditional.
199+
*
200+
* @param {string} conditional The conditional function name.
201+
* @return {Object|null} The configuration or null.
202+
*/
203+
getConfig: function( conditional ) {
204+
return acmAutocomplete.conditionals[ conditional ] || null;
205+
},
206+
207+
/**
208+
* Initialize Select2 autocomplete on an arguments input.
209+
*
210+
* @param {jQuery} $conditionalSelect The conditional select element.
211+
*/
212+
initAutocomplete: function( $conditionalSelect ) {
213+
var self = this;
214+
var conditional = $conditionalSelect.val();
215+
var config = this.getConfig( conditional );
216+
217+
if ( ! config ) {
218+
return;
219+
}
220+
221+
// Skip if Select2 is not available (CDN failed to load).
222+
if ( ! this.isSelect2Available() ) {
223+
return;
224+
}
225+
226+
var $argumentsContainer = $conditionalSelect.closest( '.conditional-single-field' ).find( '.conditional-arguments' );
227+
var $input = $argumentsContainer.find( 'input[name="acm-arguments[]"]' );
228+
var currentValue = $input.val();
229+
230+
// Hide the original input.
231+
$input.hide();
232+
233+
// Create a select element for Select2 (it requires a <select> for AJAX).
234+
var $select = $( '<select></select>' )
235+
.attr( 'name', 'acm-arguments[]' )
236+
.addClass( 'acm-autocomplete-select' );
237+
238+
// If there's an existing value, add it as an option.
239+
if ( currentValue ) {
240+
$select.append( new Option( currentValue, currentValue, true, true ) );
241+
}
242+
243+
// Insert the select after the hidden input.
244+
$input.after( $select );
245+
246+
// Remove the name from the original input to avoid duplicate submission.
247+
$input.removeAttr( 'name' ).attr( 'data-original-name', 'acm-arguments[]' );
248+
249+
// Determine the dropdown parent - use body for inline edit to avoid z-index issues.
250+
var $dropdownParent = $argumentsContainer.closest( '.acm-editor-row' ).length
251+
? $( 'body' )
252+
: $argumentsContainer;
253+
254+
// Initialize Select2 with AJAX.
255+
$select.select2({
256+
dropdownParent: $dropdownParent,
257+
ajax: {
258+
url: acmAutocomplete.ajaxUrl,
259+
dataType: 'json',
260+
delay: 250,
261+
data: function( params ) {
262+
return {
263+
action: 'acm_search_terms',
264+
nonce: acmAutocomplete.nonce,
265+
search: params.term,
266+
conditional: conditional,
267+
type: config.type,
268+
taxonomy: config.taxonomy || '',
269+
post_type: config.post_type || ''
270+
};
271+
},
272+
processResults: function( response ) {
273+
if ( response.success && response.data.results ) {
274+
return {
275+
results: response.data.results
276+
};
277+
}
278+
return { results: [] };
279+
},
280+
cache: true
281+
},
282+
minimumInputLength: acmAutocomplete.minChars,
283+
placeholder: self.getPlaceholder( conditional ),
284+
allowClear: true,
285+
tags: true, // Allow custom values (user can type their own).
286+
createTag: function( params ) {
287+
var term = $.trim( params.term );
288+
if ( term === '' ) {
289+
return null;
290+
}
291+
return {
292+
id: term,
293+
text: term,
294+
newTag: true
295+
};
296+
},
297+
language: {
298+
inputTooShort: function() {
299+
return acmAutocomplete.i18n.inputTooShort;
300+
},
301+
searching: function() {
302+
return acmAutocomplete.i18n.searching;
303+
},
304+
noResults: function() {
305+
return acmAutocomplete.i18n.noResults;
306+
},
307+
errorLoading: function() {
308+
return acmAutocomplete.i18n.errorLoading;
309+
}
310+
},
311+
width: '100%'
312+
});
313+
},
314+
315+
/**
316+
* Get placeholder text for a conditional.
317+
*
318+
* @param {string} conditional The conditional function name.
319+
* @return {string} The placeholder text.
320+
*/
321+
getPlaceholder: function( conditional ) {
322+
var placeholders = {
323+
'is_category': acmAutocomplete.i18n.searchCategories || 'Search categories...',
324+
'has_category': acmAutocomplete.i18n.searchCategories || 'Search categories...',
325+
'is_tag': acmAutocomplete.i18n.searchTags || 'Search tags...',
326+
'has_tag': acmAutocomplete.i18n.searchTags || 'Search tags...',
327+
'is_page': acmAutocomplete.i18n.searchPages || 'Search pages...',
328+
'is_single': acmAutocomplete.i18n.searchPosts || 'Search posts...'
329+
};
330+
331+
return placeholders[ conditional ] || 'Search...';
332+
},
333+
334+
/**
335+
* Check if a conditional takes no parameters.
336+
*
337+
* @param {string} conditional The conditional function name.
338+
* @return {boolean} True if the conditional takes no parameters.
339+
*/
340+
hasNoParameters: function( conditional ) {
341+
var noParamConditionals = [
342+
'is_home',
343+
'is_front_page',
344+
'is_archive',
345+
'is_search',
346+
'is_404',
347+
'is_date',
348+
'is_year',
349+
'is_month',
350+
'is_day',
351+
'is_time',
352+
'is_feed',
353+
'is_comment_feed',
354+
'is_trackback',
355+
'is_preview',
356+
'is_paged',
357+
'is_admin'
358+
];
359+
360+
return noParamConditionals.indexOf( conditional ) !== -1;
361+
},
362+
363+
/**
364+
* Update visibility of the "Add another condition" button.
365+
*
366+
* Shows the button only when at least one condition is selected.
367+
*/
368+
updateAddButtonVisibility: function() {
369+
var $form = $( '#add-adcode' );
370+
var $addButton = $form.find( '.form-add-more' );
371+
var hasSelectedCondition = false;
372+
373+
// Check if any conditional select has a value.
374+
$form.find( 'select[name="acm-conditionals[]"]' ).each( function() {
375+
if ( $( this ).val() ) {
376+
hasSelectedCondition = true;
377+
return false; // Break the loop.
378+
}
379+
});
380+
381+
if ( hasSelectedCondition ) {
382+
$addButton.addClass( 'visible' );
383+
} else {
384+
$addButton.removeClass( 'visible' );
385+
}
386+
}
387+
};
388+
389+
// Initialize when document is ready.
390+
$( document ).ready( function() {
391+
ConditionalAutocomplete.init();
392+
});
393+
394+
} )( jQuery, window.acmAutocomplete );

0 commit comments

Comments
 (0)