Skip to content

Commit 22b9457

Browse files
committed
Migrate simple event listeners to document delegation
When a new child node is added to the DOM before htmx finishes processing its parent, listeners bound to the child node could be registered twice. This is specially bad for toggle events since toggling twice results in no visible changes. Switching to delegation on document resolves it. It fixes a potential test failure in t/playwright/ticket_inline_edit.t related to the "toggling twice" behavior, where clicking inline edit icon failed to display the form.
1 parent 4eb443a commit 22b9457

File tree

4 files changed

+267
-280
lines changed

4 files changed

+267
-280
lines changed

share/static/js/assets.js

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,4 @@ htmx.onLoad(function(elt) {
1212
}
1313
});
1414
});
15-
jQuery(elt).find(".asset-create-linked-ticket").click(function(ev){
16-
ev.preventDefault();
17-
var url = this.href.replace(/\/Asset\/CreateLinkedTicket\.html\?/g,
18-
'/Asset/Helpers/CreateLinkedTicket?');
19-
20-
htmx.ajax('GET', url, '#dynamic-modal').then(() => {
21-
bootstrap.Modal.getOrCreateInstance('#dynamic-modal').show();
22-
});
23-
});
24-
jQuery(elt).find("#bulk-update-create-linked-ticket").click(function(ev){
25-
ev.preventDefault();
26-
var chkArray = [];
27-
28-
jQuery("input[name='UpdateAsset']:checked").each(function() {
29-
chkArray.push(jQuery(this).val());
30-
});
31-
32-
var selected = '';
33-
for (var i = 0; i < chkArray.length; i++) {
34-
selected += 'Asset=' + chkArray[i] + '&';
35-
}
36-
/* selected = chkArray.join(','); */
37-
var url = RT.Config.WebHomePath + '/Asset/Helpers/CreateLinkedTicket?' + selected;
38-
htmx.ajax('GET', url, '#dynamic-modal').then(() => {
39-
bootstrap.Modal.getOrCreateInstance('#dynamic-modal').show();
40-
});
41-
});
4215
});

share/static/js/event-registration.js

Lines changed: 0 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,3 @@
1-
// Disable chosing individual objects when a scrip is applied globally
2-
htmx.onLoad(function(elt) {
3-
var global_checkboxes = [
4-
"form[name=AddRemoveScrip] input[type=checkbox][name^=AddScrip-][value=0]",
5-
"form input[type=checkbox][name^=AddCustomField-][value=0]"
6-
];
7-
jQuery(elt).find(global_checkboxes.join(", "))
8-
.change(function(){
9-
var self = jQuery(this);
10-
var checked = self.prop("checked");
11-
12-
self.closest("form")
13-
.find("table.collection input[type=checkbox]")
14-
.prop("disabled", checked);
15-
});
16-
});
17-
181
// Replace user references in history with the HTML versions
192
function ReplaceUserReferences(elt) {
203
var users = jQuery(elt).find(".user[data-replace=user]");
@@ -118,71 +101,3 @@ htmx.onLoad(function(elt) {
118101
}
119102
});
120103
});
121-
122-
htmx.onLoad( function(elt) {
123-
jQuery(elt).find("input[type=file]").change( function() {
124-
var input = jQuery(this);
125-
var warning = input.next(".invalid");
126-
127-
if ( !input.val().match(/"/) ) {
128-
warning.hide();
129-
} else {
130-
if (warning.length) {
131-
warning.show();
132-
} else {
133-
input.val("");
134-
jQuery("<span class='invalid'>")
135-
.text(loc_key("quote_in_filename"))
136-
.insertAfter(input);
137-
}
138-
}
139-
});
140-
});
141-
142-
htmx.onLoad(function(elt) {
143-
jQuery(elt).find("#UpdateType").change(function(ev) {
144-
jQuery(".messagebox-container")
145-
.removeClass("action-response action-private")
146-
.addClass("action-"+ev.target.value);
147-
});
148-
});
149-
150-
htmx.onLoad(function(elt) {
151-
jQuery(elt).find('.toggle-txn-details:not(.toggle-txn-details-registered)').click(function () {
152-
return toggleTransactionDetails.apply(this);
153-
}).addClass('toggle-txn-details-registered');
154-
});
155-
156-
const article_link_focus_handler = function() {
157-
// if input focus in last row add another row of inputs
158-
const link_div = jQuery(this).parent().parent();
159-
const links_div = link_div.parent();
160-
if ( link_div.attr('data-link-number') == links_div.attr('data-link-count') ) {
161-
const link_count = parseInt( links_div.attr('data-link-count') ) + 1;
162-
links_div.attr( 'data-link-count', link_count );
163-
let new_link_div = link_div.clone();
164-
new_link_div.attr( 'data-link-number', link_count );
165-
new_link_div.find('[name^="article-link-"]').each(function(){
166-
var oldName = jQuery(this).attr('name');
167-
var newName = oldName.replace( /-\d+$/, '-' + link_count );
168-
jQuery(this).attr( 'name', newName );
169-
});
170-
new_link_div.find('[name^="article-link-"]').on( "focus", article_link_focus_handler );
171-
links_div.append(new_link_div);
172-
}
173-
};
174-
htmx.onLoad(function(elt) {
175-
jQuery('[name^="article-link-"]').on( "focus", article_link_focus_handler );
176-
});
177-
htmx.onLoad(function(elt) {
178-
jQuery(elt).find('.article-basics [name="Type"]').change(function(ev) {
179-
if ( jQuery(this).val() == 'Content' ) {
180-
jQuery('#article-type-links').addClass('hidden');
181-
jQuery('#article-type-content').removeClass('hidden');
182-
}
183-
else {
184-
jQuery('#article-type-content').addClass('hidden');
185-
jQuery('#article-type-links').removeClass('hidden');
186-
}
187-
});
188-
});

share/static/js/init.js

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,3 +360,270 @@ document.addEventListener('widgetTitleChanged', function(evt) {
360360
title.innerHTML = evt.detail.value;
361361
}
362362
});
363+
364+
/* Load the owner dropdown when the user clicks the pencil in basics */
365+
jQuery(document).on('click', '.ticket-info-basics .inline-edit-toggle.edit .rt-inline-icon', function (e) {
366+
/* htmx will run for many portlets. Only run for ticket-info-basics to avoid multiple
367+
calls to the helper for the same dropdown. */
368+
if ( e.delegateTarget.className === "ticket-info-basics" ) {
369+
var owner_dropdown_delay = jQuery('div.ticket-info-basics div.select-owner-dropdown-delay:not(.loaded)');
370+
loadOwnerDropdownDelay(owner_dropdown_delay);
371+
}
372+
});
373+
374+
jQuery(document).on('click', '.inline-edit-toggle', function (e) {
375+
e.preventDefault();
376+
e.stopPropagation();
377+
toggleInlineEdit(jQuery(this));
378+
});
379+
380+
jQuery(document).on('click', '.titlebox[data-inline-edit-behavior="click"] > .titlebox-content', function (e) {
381+
if (jQuery(e.target).is('input, select, textarea')) {
382+
return;
383+
}
384+
385+
// Bypass links, buttons and radio/checkbox controls too
386+
if (jQuery(e.target).closest('a, button, div.custom-radio, div.custom-checkbox').length) {
387+
return;
388+
}
389+
390+
e.preventDefault();
391+
e.stopPropagation();
392+
var container = jQuery(this).closest('.titlebox');
393+
if (container.hasClass('editing')) {
394+
return;
395+
}
396+
toggleInlineEdit(container.find('.inline-edit-toggle:visible'));
397+
});
398+
399+
400+
// Hide the tooltip everywhere when the element is clicked
401+
jQuery(document).on('click', '[data-bs-toggle="tooltip"]', function (e) {
402+
jQuery('[data-bs-toggle="tooltip"]').tooltip("hide");
403+
});
404+
405+
jQuery(document).on('click', 'a.delete-attach', function() {
406+
var parent = jQuery(this).closest('div');
407+
var name = jQuery(this).attr('data-name');
408+
var token = jQuery(this).closest('form').find('input[name=Token]').val();
409+
jQuery.post( RT.Config.WebHomePath + '/Helpers/Upload/Delete', { Name: name, Token: token }, function(data) {
410+
if ( data.status == 'success' ) {
411+
parent.remove();
412+
}
413+
}, 'json');
414+
return false;
415+
});
416+
417+
/* Show selected file name in UI */
418+
jQuery(document).on('change', '.custom-file input', function (e) {
419+
jQuery(this).next('.custom-file-label').html(e.target.files[0].name);
420+
});
421+
422+
423+
jQuery(document).on('input propertychange', ':input[data-type=json]', function() {
424+
var form = jQuery(this).closest('form');
425+
try {
426+
JSON.parse(jQuery(this).val());
427+
form.find('input[type=submit]').prop('disabled', false);
428+
form.find('.invalid-json').addClass('hidden');
429+
} catch (e) {
430+
form.find('input[type=submit]').prop('disabled', true);
431+
form.find('.invalid-json').removeClass('hidden');
432+
}
433+
});
434+
435+
jQuery(document).on('click', 'a.permalink', function () {
436+
htmx.ajax('GET', RT.Config.WebPath + "/Helpers/Permalink", {
437+
target: '#dynamic-modal',
438+
values: {
439+
Code: this.getAttribute('data-code'),
440+
URL: this.getAttribute('data-url')
441+
},
442+
}).then(() => {
443+
bootstrap.Modal.getOrCreateInstance('#dynamic-modal').show();
444+
});
445+
return false;
446+
});
447+
448+
// My Week auto submit
449+
jQuery(document).on('change change.td', 'div.time-tracking input[name=Date]', function () {
450+
htmx.trigger(this.closest('form'), 'submit');
451+
});
452+
453+
jQuery(document).on('change', 'div.time-tracking input[name=UserString]', function () {
454+
this.closest('form').querySelector('input[name=User]').value = this.value;
455+
htmx.trigger(this.closest('form'), 'submit');
456+
});
457+
458+
jQuery(document).on('click', 'a.search-filter', function (e) {
459+
const target = document.querySelector(e.target.closest('.search-filter').getAttribute('hx-target'));
460+
if (target.children.length > 0) {
461+
bootstrap.Modal.getOrCreateInstance(target.closest('.modal.search-results-filter')).show();
462+
}
463+
else {
464+
htmx.trigger(e.target.closest('.search-filter'), 'manual');
465+
}
466+
return false;
467+
});
468+
469+
// Automatically reveal history widget so anchor links like #txn-586 can work
470+
jQuery(document).on('click', 'a.jump-to-unread', function () {
471+
revealHistoryWidget();
472+
});
473+
474+
// Clip content
475+
jQuery(document).on('click', 'a.unclip', function() {
476+
jQuery(this).siblings('div.clip').css('height', 'auto');
477+
jQuery(this).hide();
478+
jQuery(this).siblings('a.reclip').show();
479+
return false;
480+
});
481+
482+
jQuery(document).on('click', 'a.reclip', function() {
483+
var clip_div = jQuery(this).siblings('div.clip');
484+
clip_div.height(clip_div.attr('clip-height'));
485+
jQuery(this).siblings('a.unclip').show();
486+
jQuery(this).hide();
487+
return false;
488+
});
489+
490+
jQuery(document).on('click', '.asset-create-linked-ticket', function (e) {
491+
e.preventDefault();
492+
var url = this.href.replace(/\/Asset\/CreateLinkedTicket\.html\?/g,
493+
'/Asset/Helpers/CreateLinkedTicket?');
494+
495+
htmx.ajax('GET', url, '#dynamic-modal').then(() => {
496+
bootstrap.Modal.getOrCreateInstance('#dynamic-modal').show();
497+
});
498+
});
499+
jQuery(document).on('click', '#bulk-update-create-linked-ticket', function (e) {
500+
e.preventDefault();
501+
var chkArray = [];
502+
503+
jQuery("input[name='UpdateAsset']:checked").each(function () {
504+
chkArray.push(jQuery(this).val());
505+
});
506+
507+
var selected = '';
508+
for (var i = 0; i < chkArray.length; i++) {
509+
selected += 'Asset=' + chkArray[i] + '&';
510+
}
511+
/* selected = chkArray.join(','); */
512+
var url = RT.Config.WebHomePath + '/Asset/Helpers/CreateLinkedTicket?' + selected;
513+
htmx.ajax('GET', url, '#dynamic-modal').then(() => {
514+
bootstrap.Modal.getOrCreateInstance('#dynamic-modal').show();
515+
});
516+
});
517+
518+
// Disable chosing individual objects when a scrip is applied globally
519+
jQuery(document).on('change', 'form[name=AddRemoveScrip] input[type=checkbox][name^=AddScrip-][value=0], form input[type=checkbox][name^=AddCustomField-][value=0]', function () {
520+
var self = jQuery(this);
521+
var checked = self.prop("checked");
522+
523+
self.closest("form")
524+
.find("table.collection input[type=checkbox]")
525+
.prop("disabled", checked);
526+
});
527+
528+
jQuery(document).on('change', 'input[type=file]', function () {
529+
var input = jQuery(this);
530+
var warning = input.next(".invalid");
531+
532+
if (!input.val().match(/"/)) {
533+
warning.hide();
534+
} else {
535+
if (warning.length) {
536+
warning.show();
537+
} else {
538+
input.val("");
539+
jQuery("<span class='invalid'>")
540+
.text(loc_key("quote_in_filename"))
541+
.insertAfter(input);
542+
}
543+
}
544+
});
545+
546+
jQuery(document).on('change', '#UpdateType', function (e) {
547+
jQuery(".messagebox-container")
548+
.removeClass("action-response action-private")
549+
.addClass("action-" + e.target.value);
550+
});
551+
552+
jQuery(document).on('click', '.toggle-txn-details', function (e) {
553+
return toggleTransactionDetails.apply(this);
554+
});
555+
556+
jQuery(document).on('change', '.article-basics [name="Type"]', function () {
557+
if (jQuery(this).val() == 'Content') {
558+
jQuery('#article-type-links').addClass('hidden');
559+
jQuery('#article-type-content').removeClass('hidden');
560+
}
561+
else {
562+
jQuery('#article-type-content').addClass('hidden');
563+
jQuery('#article-type-links').removeClass('hidden');
564+
}
565+
});
566+
567+
jQuery(document).on('focus', '[name^="article-link-"]', function () {
568+
// if input focus in last row add another row of inputs
569+
const link_div = jQuery(this).parent().parent();
570+
const links_div = link_div.parent();
571+
if (link_div.attr('data-link-number') == links_div.attr('data-link-count')) {
572+
const link_count = parseInt(links_div.attr('data-link-count')) + 1;
573+
links_div.attr('data-link-count', link_count);
574+
let new_link_div = link_div.clone();
575+
new_link_div.attr('data-link-number', link_count);
576+
new_link_div.find('[name^="article-link-"]').each(function () {
577+
var oldName = jQuery(this).attr('name');
578+
var newName = oldName.replace(/-\d+$/, '-' + link_count);
579+
jQuery(this).attr('name', newName);
580+
});
581+
links_div.append(new_link_div);
582+
}
583+
});
584+
585+
// Automatically sync to set input values to ones in config files.
586+
jQuery(document).on('change', 'form[name=EditConfig] input[name$="-file"]', function (e) {
587+
var file_input = jQuery(this);
588+
var form = file_input.closest('form');
589+
var file_name = file_input.attr('name');
590+
var file_value = form.find('input[name=' + file_name + '-Current]').val();
591+
var checked = jQuery(this).is(':checked') ? 1 : 0;
592+
if ( !checked ) return;
593+
594+
var db_name = file_name.replace(/-file$/, '');
595+
var db_input = form.find(':input[name=' + db_name + ']');
596+
var db_input_type = db_input.attr('type') || db_input.prop('tagName').toLowerCase();
597+
if ( db_input_type == 'radio' ) {
598+
db_input.filter('[value=' + (file_value || 0) + ']').prop('checked', true);
599+
}
600+
else if ( db_input_type == 'select' ) {
601+
// Silently update value, otherwise the radio would be unchecked again because of select's change event.
602+
db_input.get(0).tomselect.setValue(file_value.length ? file_value : '__empty_value__', true);
603+
}
604+
else {
605+
db_input.val(file_value);
606+
}
607+
});
608+
609+
jQuery(document).on('change', 'form[name=BuildQuery] select[name^=SelectCustomField]', function() {
610+
var form = jQuery(this).closest('form');
611+
var row = jQuery(this).closest('div.row');
612+
var val = jQuery(this).val();
613+
614+
var new_operator = form.find(':input[name="' + val + 'Op"]:first').clone();
615+
new_operator.attr('id', null).removeClass('tomselected ts-hidden-accessible');
616+
row.children('div.rt-search-operator').children().remove();
617+
row.children('div.rt-search-operator').append(new_operator);
618+
619+
var new_value = form.find(':input[name="ValueOf' + val + '"]:first');
620+
new_value = new_value.clone();
621+
622+
new_value.attr('id', null).removeClass('tomselected ts-hidden-accessible');
623+
row.children('div.rt-search-value').children().remove();
624+
row.children('div.rt-search-value').append(new_value);
625+
if ( new_value.hasClass('datepicker') ) {
626+
initDatePicker(row.get(0));
627+
}
628+
initializeSelectElements(row.get(0));
629+
});

0 commit comments

Comments
 (0)