43

I have an interesting situation - I have a table row which, currently, shows it's hidden counterpart when I click the "Expand" button. The original (unhidden) row which contains the expand button also has some content in a certain cell which, when clicked, becomes editable. I would like to be rid of the expand button, and enable expanding of the row via doubleclick anywhere in the row itself, including the field that turns editable when you click it. You can smell the trouble here already.

When I double click a row, two click events are fired first, before the dblclick occurs. This means if I double click the field, it will turn into an editable one, and the row will expand. I would like to prevent this. I want the doubleclick to prevent the firing of the single click, and the single click to perform as usual.

Using event.stopPropagation() clearly won't work since they're two different events.

Any ideas?

Edit (some semi-pseudo code):

Original version:

<table>
    <tbody>
        <tr>
            <td><a href="javascript:$('#row_to_expand').toggle();" title="Expand the hidden row">Expand Row</a></td>
            <td>Some kind of random data</td>
            <td><?= $editable_cell_which_turns_into_an_input_field_on_single_click[0]->value("First editable value") ?></td>
            <td><?= $editable_cell_which_turns_into_an_input_field_on_single_click[1]->value("Second editable value") ?></td>
            <td><?= $editable_cell_which_turns_into_an_input_field_on_single_click[2]->value("Third editable value") ?></td>
            <!-- ... -->
            <td><?= $editable_cell_which_turns_into_an_input_field_on_single_click[n]->value("Nth editable value") ?></td>
        </tr>
        <tr style="display: none" id="row_to_expand">
            <td colspan="n">Some hidden data</td>
        </tr>
    </tbody>
</table>

Desired version:

<table>
    <tbody>
        <tr ondblclick="$('#row_to_expand').toggle()">
            <td>Some kind of random data</td>
            <td><?= $editable_cell_which_turns_into_an_input_field_on_single_click[0]->value("First editable value") ?></td>
            <td><?= $editable_cell_which_turns_into_an_input_field_on_single_click[1]->value("Second editable value") ?></td>
            <td><?= $editable_cell_which_turns_into_an_input_field_on_single_click[2]->value("Third editable value") ?></td>
            <!-- ... -->
            <td><?= $editable_cell_which_turns_into_an_input_field_on_single_click[n]->value("Nth editable value") ?></td>
        </tr>
        <tr style="display: none" id="row_to_expand">
            <td colspan="n">Some hidden data</td>
        </tr>
    </tbody>
</table>

Cheers

Swader
  • 11,387
  • 14
  • 50
  • 84
  • 3
    You could try delaying the click-handler using setTimeout and then raise a "doubleclick-has-happened" flag on double clicks and check for that flag in your single click handler – Thor Jacobsen Mar 29 '11 at 10:55
  • @diEcho Done! @freaktm Thanks, I considered this, but it seems unpractical and hackish, especially since I would have to modify the click handler for my editable cell helper site-wide. I will probably use this as a last resort, yes, but would still like to know if someone has a better solution. – Swader Mar 29 '11 at 11:03
  • If you get there, I've made an example for you: http://jsfiddle.net/nslr/2TvgE/ But yes, not very pretty :) – Thor Jacobsen Mar 29 '11 at 11:05
  • Hmm, tried it, didn't seem to work as planned. When I doubleclick, I get doubleclick, and when I single click I get single click. But when I double click after doubleclick, I get both single click and double click :) – Swader Mar 29 '11 at 11:12
  • possible duplicate of [Jquery bind double click and single click separately](http://stackoverflow.com/questions/6330431/jquery-bind-double-click-and-single-click-separately) – Popnoodles Aug 20 '14 at 08:34

10 Answers10

24

The general idea:

  1. Upon the first click, dont call the associated function (say single_click_function()). Rather, set a timer for a certain period of time(say x). If we do not get another click during that time span, go for the single_click_function(). If we do get one, call double_click_function()

  2. Timer will be cleared once the second click is received. It will also be cleared once x milliseconds are lapsed.

BTW, check Paolo's reply out: Need to cancel click/mouseup events when double-click event detected and of course the entire thread! :-)

Better answer: https://stackoverflow.com/a/7845282/260610

Community
  • 1
  • 1
UltraInstinct
  • 43,308
  • 12
  • 81
  • 104
  • Thanks, this worked, but I decided against it in the end. I found some better solutions for usability and went with those. – Swader Apr 01 '11 at 10:42
  • 12
    @Swader can u share your solution what workaround u choose b/c i also don't want to use the settimeout(...) method. – zaree Nov 19 '13 at 05:44
  • 5
    @Swader is pulling a Fermat – Dan Jul 21 '14 at 16:58
  • 2
    I guess this dude didn't ever post the better solution...? – Brian Stinar Jul 29 '14 at 19:43
  • @BrianStinar I would have -- if OP had posted his Javascript attempts at solving the problem. HTML pseudo-structure, or a verbose explanation do not count. – UltraInstinct Jul 30 '14 at 03:38
  • 1
    I imagine that @Swader decided on a UX design that didn't require binding both single-click and double-click on the same element. This gets more complicated when you want it to work on desktop and mobile, mapping tap-touch-hold to click-dblclick-context. I know it's probably unwise, but am just so tempted. – prototype Apr 10 '15 at 02:43
  • 1
    @user645715 correct, I went without this. Mobile didn't matter much for us as this was still in the desktop era and for an internal system for employees, but I eventually went with a better designed interface. – Swader Apr 10 '15 at 05:54
12

Working demo

$('#alreadyclicked').val('no click');
$('td.dblclickable').on('click',function(){
    var $button=$(this);
    if ($button.data('alreadyclicked')){
        $button.data('alreadyclicked', false); // reset
        $('#alreadyclicked').val('no click');

        if ($button.data('alreadyclickedTimeout')){
            clearTimeout($button.data('alreadyclickedTimeout')); // prevent this from happening
        }
        // do what needs to happen on double click. 
        $('#action').val('Was double clicked');
    }else{
        $button.data('alreadyclicked', true);
        $('#alreadyclicked').val('clicked');
        var alreadyclickedTimeout=setTimeout(function(){
            $button.data('alreadyclicked', false); // reset when it happens
            $('#alreadyclicked').val('no click');
            $('#action').val('Was single clicked');
            // do what needs to happen on single click. 
            // use $button instead of $(this) because $(this) is 
            // no longer the element
        },300); // <-- dblclick tolerance here
        $button.data('alreadyclickedTimeout', alreadyclickedTimeout); // store this id to clear if necessary
    }
    return false;
});

obviously use <td class="dblclickable">

Popnoodles
  • 28,090
  • 2
  • 45
  • 53
  • I cranked it to 1300, and it simply delays my alert of showing 'in single' by another second no matter how fast i click. – BReal14 Apr 12 '13 at 15:00
  • Actually, If i click SLOWLY I can get the dbl to fire with the 1300 window. click 1 = 0, click 2 = 500ms later (-ish) time frames. 2 slow diliberate clicks. is the time it takes to go through the JS file (a bunch of stuff in a function(){} wrapper for onload waiting causing it? – BReal14 Apr 12 '13 at 15:05
  • No, my logic is sound http://jsfiddle.net/V8twU/ Did you put your procedure for double-click event in the right place? – Popnoodles Apr 12 '13 at 18:12
  • Have you tried it in ie8? it doesn't seem to fire correctly even with this fiddle for me. – BReal14 Apr 12 '13 at 19:34
  • heh, i'd love to. unfortunately, thats the 'environment' here and its the only browser in use still. :( – BReal14 Apr 16 '13 at 21:01
  • Works great! I use 250 ms for a close-to-familiar dbl-click speed – Jonathan Mar 20 '14 at 15:13
4

I wrote a simple jQuery plugin that lets you use a custom 'singleclick' event to differentiate a single-click from a double-click. This allows you to build an interface where a single-click makes the input editable while a double-click expands it. Much like Windows/OSX filesystem rename/open UI.

https://github.com/omriyariv/jquery-singleclick

$('#someInput').on('singleclick', function(e) {
    // The event will be fired with a small delay but will not fire upon a double-click.
    makeEditable(e.target);
}

$('#container').on('dblclick', function(e) {
    // Expand the row...
}
omriyariv
  • 151
  • 5
3

Rewrite user822711's answer for reuse:

  $.singleDoubleClick = function(singleClk, doubleClk) {
    return (function() {
      var alreadyclicked = false;
      var alreadyclickedTimeout;

      return function(e) {
        if (alreadyclicked) {
          // double
          alreadyclicked = false;
          alreadyclickedTimeout && clearTimeout(alreadyclickedTimeout);
          doubleClk && doubleClk(e);
        } else {
          // single
          alreadyclicked = true;
          alreadyclickedTimeout = setTimeout(function() {
            alreadyclicked = false;
            singleClk && singleClk(e);
          }, 300);
        }
      }
    })();
  }

Call the function.

$('td.dblclickable').bind('click', $.singleDoubleClick(function(e){
  //single click.
}, function(e){
  //double click.
}));
Fracu
  • 835
  • 1
  • 13
  • 28
puttyshell
  • 31
  • 1
2

This example in vanilla javascript should work:

var timer = 0
var delay = 200
var prevent = false

node.onclick = (evnt) => {
    timer = setTimeout(() => {
         if(!prevent){
             //Your code
         }
         prevent = false
    }, delay)
}
node.ondblclick = (evnt) => {
    clearTimeout(timer)
    prevent=true
    //Your code
}
2

The issue only occurs when the editable field is clicked, so attach a regular click handler to that element which will cancel propagation of the event (see stopPropagation) and will set a timeout (setTimeout(...)) for, say, 600ms (default time between two clicks to be deemed a dbl-click is 500ms [src]). If, by that time the dblclick has not occurred (you can have a var accessible in both event handlers that acts as a flag to detect this) then you can assume the user wants to expand the row instead and continue with that action...

IMO, you should re-think this. Alas, a single click handler cannot know if the user is about to double-click.

I suggest making both actions single click, and simply don't propagate up from the editable field when it is clicked.

James
  • 109,676
  • 31
  • 162
  • 175
1
window.UI = {
  MIN_LAPS_FOR_DBL_CLICK: 200, // msecs

  evt_down:null,
  timer_down:null,
  mousedown:function(evt){
    if( this.evt_down && evt.timeStamp < this.evt_down.timeStamp + this.MIN_LAPS_FOR_DBL_CLICK ){
      clearTimeout(this.timer_down);
      this.double_click(evt); // => double click
    } else {
      this.evt_down   = evt;
      this.timer_down = setTimeout("UI.do_on_mousedown()", this.MIN_LAPS_FOR_DBL_CLICK);
    }
  },
  evt_up:null,
  timer_up:null,
  mouseup:function(evt){
    if( this.evt_up && evt.timeStamp < this.evt_up.timeStamp + this.MIN_LAPS_FOR_DBL_CLICK ){
      clearTimeout(this.timer_up);
    } else {
      this.evt_up   = evt;
      this.timer_up = setTimeout("UI.do_on_mouseup()", this.MIN_LAPS_FOR_DBL_CLICK);
    }
  },
  do_on_mousedown:function(){
    var evt = this.evt_down;
    // Treatment MOUSE-DOWN here
  },
  do_on_mouseup:function(){
    var evt = this.evt_up;
    // Treatment MOUSE-UP here
  },
  double_click:function(evt){
    // Treatment DBL-CLICK here
  }

}

// Then the observers

$('div#myfoo').bind('mousedown',  $.proxy(UI.mousedown, UI));
$('div#myfoo').bind('mouseup',    $.proxy(UI.mouseup, UI));
0

Adding to answer from @puttyshell, this code has space for a visualSingleClick function. This run before setTimeout and is intended to show user some visual action for the action before the timeout, and with that I can use a greater timeout. I'm using this on a Google Drive browsing interface to show user that file or folder that he clicked is selected. Google Drive itself does something similar, but don't saw it code.

/* plugin to enable single and double click on same object */
$.singleDoubleClick = function(visualSingleClk, singleClk, doubleClk) {
  return (function() {
    var alreadyclicked = false;
    var alreadyclickedTimeout;

    return function(e) {
      if (alreadyclicked) {
        // double
        alreadyclicked = false;
        alreadyclickedTimeout && clearTimeout(alreadyclickedTimeout);
        doubleClk && doubleClk(e);
      } else {
        // single
        alreadyclicked = true;
        visualSingleClk && visualSingleClk(e);
        alreadyclickedTimeout = setTimeout(function() {
          alreadyclicked = false;
          singleClk && singleClk(e);
        }, 500);
      }
    }
  })();
}
chronossc
  • 425
  • 4
  • 3
0

Here's how I handle this in jquery:

async function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}
let clickCount = 0;
$(`#btnDebug`).on(`click`, async function() {
    clickCount++;
    if (clickCount > 1) {
        // handle our double-click event
        console.log(`got double click`);
        /*
        ...do stuff
        */
        clickCount = -1;
        return;
    }
    await sleep(300);
    if (clickCount === -1) {
        // we handled a double-click event so set everything back and exit
        clickCount = 0;
        return;
    }
    // handle our single-click event. We made it past our 300ms sleep and we didn't get another click so we're assuming a single-click.
    clickCount = 0;
    console.log(`got single click`);
        /*
        ...do stuff
        */
});

I realize I can do a setTimeout instead of sleep but I use that sleep function fairly often so I just used it here.

Chris
  • 650
  • 7
  • 20
-2

There is a more elegant way handling this issue if you're using backbonejs:


    initialize: function(){
        this.addListener_openWidget();
    }

    addListener_openWidget: function(){
        this.listenToOnce( this, 'openWidgetView', function(e){this.openWidget(e)} );
    },

    events: {
        'click #openWidgetLink' : function(e){this.trigger('openWidgetView',e)},
        'click #closeWidgetLink' : 'closeWidget'
    },

    openWidget: function( e ){
       //Do your stuff
    },

    closeWidget: function(){
       this.addListener_openWidget();
    }