I'm having a problem writing event handling code for a custom checkbox control, and I'm hoping that someone here might be able to help.
The basic event handling for toggling the checkbox works fine; the problem lies with trying to match behaviour of proper checkbox controls as closely as possible, specifically in relation to interaction with other event handlers that may be attached to one of my checkboxes and which may call preventDefault().
Experimenting with a proper checkbox, with both an event handler attached directly to the control, and a delegate one created with .on(), if preventDefault() is called, it blocks the toggle of the checkmark. If one of these event handlers calls a function with setTimeout(), which calls preventDefault(), this is ignored and the checkmark is toggled. This is the behaviour I wish to emulate.
As can be seen with the code below, I tackle this by toggling the checkbox state, then doing a check of isDefaultPrevented() on the event within a setTimeout() call, which should execute immediately after all event handlers have executed (including bubbling). If any event handler called preventDefault() then I toggle the checkmark back again.
When an event handler (test #1) is attached directly to the checkbox, and calls preventDefault(), this works perfectly as intended. The problem lies with the delegated event handler, as in test #2. With the delegated event handler, if I click on the checkbox's label, it works perfectly as intended (change cancelled), however if I click directly on the checkbox, or set focus to it and press the space bar, it does not, the checkmark is toggled...
Behaviour is identical across Firefox, Chrome, Opera, IE8 and IE9, so it must be either something I'm doing or a bug in jQuery.
The custom checkbox control is as follows:
- <div class="w-checkbox">
- <div class="w-checkbox-checkmark" role="checkbox" aria-checked="false" aria-labelledby="lbl-1" tabindex="0"></div>
- <div class="w-checkbox-label" id="lbl-1">Test</div>
- <input type="checkbox" name="test1" class="w-checkbox-value" style="display:none;" />
- </div>
The jQuery code is as follows:
- $(function() {
- // Fix for IE
- // Unless the IE debugger is open, 'console' evaluates to undefined, and hence any console logging/debugging
- // code lines will cause things to fail!
- if (!window.console) console = { log: function() {} };
-
- /* Checkbox Custom Input Control Widget */
- $('div.w-checkbox').each(function(index, elem) {
- $inst = $(this);
- var $cm = $inst.find('div.w-checkbox-checkmark').eq(0); //Checkmark
- var $cml = $inst.find('div.w-checkbox-label').eq(0); //Checkmark label
-
- // Register toggle status events
- $cm.on('click', {cm:$cm}, function(event) { action(event, event.data.cm); });
- $cm.on('keydown', {cm:$cm}, function(event) { if (keyPressFilter(event, event.data.cm)) { action(event, event.data.cm); } }); //Certain key presses (SPACE key) when checkmark has focus should toggle it!
- $cm.on('keypress', {cm:$cm}, function(event) { if (keyPressFilter(event, event.data.cm)) { return false; } }); //Cancel default action for keypress events, else pressing SPACE to toggle a checkmark jumps the page around also
- $cml.on('click', {cm:$cm}, function(event) { event.data.cm.trigger('click'); }); //Clicking on label should trigger action on checkmark (best to trigger click event so all event handlers run)
-
- // Register hover events
- // These are registered on the label and should change the appearance of the checkbox on hovering over the label
- $cml.on('mouseenter', {cm:$cm}, function() {
- if ($cm.hasClass('w-checkbox-disabled')) { return; }
- $cm.addClass('w-checkbox-hover');
- });
- $cml.on('mouseleave', {cm:$cm}, function() {
- if ($cm.hasClass('w-checkbox-disabled')) { return; }
- $cm.removeClass('w-checkbox-hover');
- });
-
- var keyPressFilter = function(event, $cm) {
- if (event.keyCode && event.keyCode === $.ui.keyCode.SPACE) { return true; }
- return false;
- };
-
- var action = function(event, $cm) {
-
- // Ignore event if checkbox disabled
- if ($cm.hasClass('w-checkbox-disabled')) { return; }
-
- var $control = $cm.closest('div.w-checkbox');
- var $cmv = $control.find('input.w-checkbox-value').eq(0);
-
- var toggle = function() {
- if ($cm.hasClass('w-checkbox-checked')) {
- $cm.removeClass('w-checkbox-checked').attr('aria-checked', 'false');
- $cmv.attr('checked', null);
- } else {
- $cm.addClass('w-checkbox-checked').attr('aria-checked', 'true');
- $cmv.attr('checked', 'checked');
- }
- // Fix for IE, you need to shift focus so that the control updates, otherwise user must click something else to trigger blur event
- $('body').focus();
- $cm.focus();
- };
-
- toggle(); //Change checkbox state
- // Reverse the action if another separate event cancels the action
- // A timeout even is used here to ensure this check is performed after all event handlers for the event have run. Should any
- // other event handler cancel the default action, reverse the change. Note, this is safe because even with standard checkboxes,
- // if an event handler uses a settimeout and tries to cancel the event in it, the cancellation is ignored!
- setTimeout(function() {
- if (event.isDefaultPrevented()) {
- toggle();
- }
- }, 0);
- };
- });
-
- /* Test #1 */
- /*$('div.w-checkbox-checkmark').on('click', function(event) { event.preventDefault(); });*/
-
- /* Test #2 */
- $('body').on('click', 'div.w-checkbox-checkmark', function(event) { event.preventDefault(); });
- });
Any ideas?