3

I want to do the ill-advised and place both an onClick and onDoubleClick on the same element with each type of event resulting in a different action. Specifically on an image, click to advance to the next image, double-click to toggle fullscreen.

Naturally I get two clicks followed by a double-click (though I understand that some browsers only fire one click before the double-click).

I had thought to make it easy on myself and place each event into a buffer (List) - or rather to add the event.type string to a list, then, after a suitable elapse of time, say 250 or 300 milliseconds examine the last item in the buffer and if doubleclick then go fullscreen else advance the length of the list.

I have found that the list only ever has one item, and I have not worked out how to get the timer to work..

Amongst my attempts was this one:

void catchClickEvents(Event e) {
  var eventTypes = new List<String>();
  eventTypes.add(e.type);
  Duration duration = const Duration(milliseconds: 300);
  var timeout = new Timer(duration, () => processEvents(eventTypes));
}

void processEvents(List eTypes) {
  // just to see what is going on...
  print(eTypes);
}

this results in this output printed to the console:

[click]
[click]
[dblclick]

rather than

[click, click, dblclick]

If I slow it down there is a clear delay before those three event types are printed together

So...

The bigger question is 'What is the darty way to distiguish between single and double-click and perform a different action for each?'

The other questions are:

How do I fill a buffer with successive events (and later clear it down) - or even how do I use Dart's Stream of events as a buffer...

What is the real way timeout before examining the contents of the buffer?

(and I guess the final question is 'should I abandon the effort and settle for a conventional set of buttons with glyph-icons?'!)

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567

3 Answers3

1

Your page should react on user inputs as fast as possible. If you wait to confirm double click - your onClick will become much less responsive. You can hide the problem by changing visual state of the element(for example, playing animation) after first click but it can confuse user. And it gets even worse with handheld. Also if element has to react only on onClick event, you can "cheat" and listen to onmousedown instead - it will make your UI much more responsive.

On top of all this, double click, from client to client, may have noticeably different trigger time interval and it isn't intuitive, for user, that you can double click something. You will have to bloat your interface with unnecessary hints.

edit: Added my solution. It should be fairly extensible and future proof.

import 'dart:html';
import 'dart:async';
import 'dart:math';

//enum. Kinda... https://code.google.com/p/dart/issues/detail?id=88
class UIAction {
  static const NEXT = const UIAction._(0);
  static const FULLSCREEN = const UIAction._(1);
  static const ERROR = const UIAction._(2);
  final int value;
  const UIAction._(this.value);
}

/**
 *[UIEventToUIAction] transforms UIEvents into corresponding  UIActions.
 */
class UIEventToUIAction implements StreamTransformer<UIEvent, UIAction> {

/**
 * I use "guesstimate" value for [doubleClickInterval] but you can calculate
 * comfortable value for the user from his/her previous activity.
 */
  final Duration doubleClickInterval = const Duration(milliseconds: 400);

  final StreamController<UIAction> st = new StreamController();

  Stream<UIAction> bind(Stream<UIEvent> originalStream) {
    int t1 = 0,
        t2 = 0;
    bool isOdd = true;
    Duration deltaTime;
    originalStream.timeout(doubleClickInterval, onTimeout: (_) {
      if ((deltaTime != null) && (deltaTime >= doubleClickInterval)) {
        st.add(UIAction.NEXT);
      }
    }).forEach((UIEvent uiEvent) {
      isOdd ? t1 = uiEvent.timeStamp : t2 = uiEvent.timeStamp;
      deltaTime = new Duration(milliseconds: (t1 - t2).abs());
      if (deltaTime < doubleClickInterval) st.add(UIAction.FULLSCREEN);
      isOdd = !isOdd;
    });
    return st.stream;
  }
}


void main() {

  //Eagerly perform time consuming tasks to decrease the input latency.
  Future NextImageLoaded;
  Future LargeImageLoaded;
  element.onMouseDown.forEach((_) {
    NextImageLoaded = asyncActionStub(
        "load next image. Returns completed future if already loaded");
    LargeImageLoaded = asyncActionStub(
        "load large version of active image to show in fullscreen mode."
            "Returns completed future if already loaded");
  });


  Stream<UIEvent> userInputs = element.onClick as Stream<UIEvent>;

  userInputs.transform(new UIEventToUIAction()).forEach((action) {
    switch (action) {
      case UIAction.FULLSCREEN:
        LargeImageLoaded.then( (_) =>asyncActionStub("fullscreen mode") )
        .then((_) => print("'full screen' finished"));
        break;
      case UIAction.NEXT:
        NextImageLoaded.then( (_) =>asyncActionStub("next image") )
        .then((_) => print("'next image' finished"));
        break;
      default:
        asyncActionStub("???");
    }
  });
}


final DivElement element = querySelector("#element");

final Random rng = new Random();
final Set performed = new Set();
/**
 *[asyncActionStub] Pretends to asynchronously do something usefull.
 * Also pretends to use cache.
 */
Future asyncActionStub(String msg) {
  if (performed.contains(msg)) {
    return new Future.delayed(const Duration(milliseconds: 0));
  }
  print(msg);
  return new Future.delayed(
      new Duration(milliseconds: rng.nextInt(300)),
      () => performed.add(msg));
}
JAre
  • 4,666
  • 3
  • 27
  • 45
  • 1
    I am inclined to agree with you JAre - apart from chasing around after all the possibilities provided by user preferences, OS implementations, Browser implementations and so on, I found it remarkably easy (as a user) to create more variations with my impatience - so your remark 'your page should react on user inputs as fast as possible' wins the day. Shame though - I liked the simplicity of the instruction 'click to advance image; double-click to toggle fullscreen' - So back to glyph-icons... – theTechnaddict Jul 12 '14 at 11:27
  • 1
    I mentioned the click timing settings because, for example, if you are hardcore gamer or caffeine addict, then your d-click setting could be set to overdrive. But if you are 90 year old grandma with rheumatoid arthritis - you, probably, not too "clicky". Or, for example, if your handheld device has really bad touch detection, it has huge delay or misses half the inputs, or your hands are frozen. Imagine, you're trying to doubleClick and instead of expected toggle fullscreen behavior - the image just changes to the next one. So better solution will be "swipe gesture" to navigate between images. – JAre Jul 12 '14 at 15:32
1

The problem is that your variable is not global.

var eventTypes = new List<String>();

void catchClickEvents(Event e) {
  eventTypes.add(e.type);
  Duration duration = const Duration(milliseconds: 300);
  var timeout = new Timer(duration, () => processEvents(eventTypes));
}

void processEvents(List eTypes) {
  print(eTypes);
}

There also is e.detail that should return the number of the click. You can use that, if you don't need the Internet Explorer. The problem with your list is that it grows and never gets cleared.

Let's take into account what we know: You get click events and at somepoint you have doubleclicks.

You could use a click counter here. (Or use e.detail) to skip the second click event. So you only have click and dblclick.

Now when you get a click event, you create a new timer like you did before and run the click action. If you get the dblclick event you simply run you action. This could like this:

DivElement div = querySelector('#div');
Timer timeout = null;
div.onClick.listen((MouseEvent e) {
  if(e.detail >= 2) {
    e.preventDefault();
  } else {
    if(timeout != null) {
      timeout.cancel();
    }

    timeout = new Timer(new Duration(milliseconds: 150), () => print('click'));
  }
});

div.onDoubleClick.listen((MouseEvent e) {
  if(timeout != null) {
    timeout.cancel();
    timeout = null;
  }
  print('dblclick');
});

This is the example code that works for me. If you can't rely on e.detail just us a counter and reset it after some ms after a click event.

I hope this helps you.

Regards, Robert

Robert
  • 5,484
  • 1
  • 22
  • 35
  • Using a global enables me to populate a list, although that approach still has the problem of calling the callback at the end of the duration three times - detail is not available as a getter - so I was not able to reproduce your code - but in any case I can't ignore IE (regrettably)... - Given the variables of numbers of clicks returned by browsers, and time between clicks I'm beginning to think this is a fool's errand. Many thanks for advancing my understanding of Dart on a more general level, Robert - much appreciated.. – theTechnaddict Jul 12 '14 at 11:20
  • Timers are dangerous. If the callback function in the `new Timer(..., callback)` will trow an error and you don't catch it - application will die. This is one of the reasons why it's better to work in terms of `Futures` – JAre Jul 12 '14 at 13:28
  • Futures dont make any sense here, or? I want a 'hard' timer here - I personally never had problems with Timers in Dart. Simply add try/catch to my code and it should work. – Robert Jul 12 '14 at 14:15
  • You can replace the `Timer` with `Future.delayed` and the error message will be nicely wrapped. "Try catch" will do, but the standard error handling(stack unwinding) doesn't fit well in async paradigm. – JAre Jul 12 '14 at 15:04
  • 1
    oh okay. Didn't new that. Thank you :) – Robert Jul 12 '14 at 15:11
  • @JAre I haven't found a way to cancel a `Future.delayed` which is necessary here (as in my answer). – Günter Zöchbauer Jul 13 '14 at 11:36
  • @GünterZöchbauer new Future.delayed(const Duration(seconds: 3)).asStream().listen((_)=>print('hello'))..cancel(); It's ugly but working. – JAre Jul 18 '14 at 08:30
  • @JAre Thanks for the update! Have you tried if cancel this way actually prevents the execution of the passed closure? – Günter Zöchbauer Jul 18 '14 at 08:33
  • @GünterZöchbauer It cancels the subscription to the stream. It won't affect future. That's how the safe timer wrapped in a future will look like https://gist.github.com/anonymous/d7b24edeba4b2a993487 I took `Future.delayed` implementation as a base. Now it can be canceled outside. – JAre Jul 18 '14 at 09:27
  • @JAre Do you think this has an advantage over using the Timer directly like above? – Günter Zöchbauer Jul 18 '14 at 09:31
  • @GünterZöchbauer `new Timer(new Duration(milliseconds: 150), () => throw("I think it will kill my app"));` And if error is contained in a future I can pass it around in the async manner. – JAre Jul 18 '14 at 09:36
  • @GünterZöchbauer http://goo.gl/6X6v7s *doesn't work properly in the dev build - almost drove me crazy* I think, the main difference between the timer callback with try-catch and the future is in the direction of the error propagation. With a try-catch block error will bubble up to the even listener(part of UI) so it should have some specific knowledge about the module from which this error originate to handle it. But with future - the error will propagate forward until it finds someone with sufficient knowledge to process it. And since it moves with the data flow, it needs less boilerplate. – JAre Jul 18 '14 at 16:38
  • @JAre that was not my intention ;-) As you mentioned I remember having read that with a Future the passed closure/function is executed in a zone but not with Timer. I guess this is what causes the difference in error propagation. – Günter Zöchbauer Jul 18 '14 at 16:40
  • @GünterZöchbauer I wasted like 3 hours figuring out why `onError` in the `Future` can't catch the error and how the hell I hadn't noticed this behaviour before and why it doesn't make any sense and why I can't comprehend its logic. `Dart SDK version 1.6.0-dev.3.0` trolled me damn hard D: – JAre Jul 18 '14 at 16:46
  • @JAre Has this changed in 1.6.0-dev.x or is there a bug? – Günter Zöchbauer Jul 18 '14 at 16:47
  • @GünterZöchbauer I don't know for sure what's causing it but the code works as expected in `Dart SDK version 1.5.3` – JAre Jul 18 '14 at 16:50
  • @JAre can we connect at G+ so we don't have to use comments for chat? You find my G+ link in my SO profile. – Günter Zöchbauer Jul 18 '14 at 16:54
1

I'm not sure if IE still has the event sequence explained here (no 2nd click event) https://stackoverflow.com/a/5511527/217408

If yes you can use a slightly deviated variant of Roberts solution:

library app_element;

import 'dart:html' as dom;
import 'dart:async' as async;

Duration dblClickDelay = new Duration(milliseconds: 500);
async.Timer clickTimer;

void clickHandler(dom.MouseEvent e, [bool forReal = false]) {
  if(clickTimer == null) {
    clickTimer = new async.Timer(dblClickDelay, () {
      clickHandler(e, true);
      clickTimer = null;
    });
  } else if(forReal){
    print('click');
  }
}

void dblClickHandler(dom.MouseEvent e) {
  if(clickTimer != null) {
    clickTimer.cancel();
    clickTimer = null;
  }
  print('doubleClick');
}

void main() {
  dom.querySelector('button')
    ..onClick.listen(clickHandler)
    ..onDoubleClick.listen(dblClickHandler);
}

or just use Roberts solution with the mouseUp event instead of the click event.

Community
  • 1
  • 1
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • Class `Stream` has `timeout` you can use it instead of another `Timer` – JAre Jul 12 '14 at 10:51
  • 1
    @JAre That's interesting, thanks for the hint! I'll take a look how this can be used. – Günter Zöchbauer Jul 12 '14 at 10:59
  • You right, I will. It might work only in my head, like most of the times. *deleted my rambling* – JAre Jul 12 '14 at 11:18
  • 1
    I didn't say it doesn't work (haven't had a close look yet), but I hoped to get a working solution without spending too much time ;-) – Günter Zöchbauer Jul 12 '14 at 11:20
  • I took a look but didn't find a solution that makes use of `..onClick.timeout`. Might prove useful in other situations though. Thanks again for pointing it out. – Günter Zöchbauer Jul 13 '14 at 11:38
  • i shoved `timeout` into my code, but now it looks like a clever code instead of useful one. I think, it's for the error handling and not to be used as a part of the core logic. Without `onTimeout` it will throw and `onTimeout` gets fired during initialization of my code + i have to listen the Stream that `timeout` returns. – JAre Jul 13 '14 at 11:47