Showing posts with label jquery. Show all posts
Showing posts with label jquery. Show all posts

Wednesday, June 25, 2014

Casper, d3, jquery and clicking

I spent (wasted? invested?) an awful lot of time yesterday on trying to use CasperJS to click a link inside an SVG diagram that had been made by d3.js. I’ll start this article by showing my Foolish Mistake, but then will document what I did learn.

Foolishness

Normally, in CasperJS, we write this.click('#myButton'), where “#myButton” can be just about any CSS selector.
Aside: it is normally this.click() rather than casper.click()
because it is normally in the handler function of a casper.waitXXX()
function.
This wasn’t working for me trying to click an SVG <g> tag, that starts a search running. I was taking a screenshot 0.5s later to check it had worked, and it showed the page had not changed.
And that turned out to be foolish. The click was working, the search was working, d3 and SVG had nothing to do with anything. The problem was simply were no search results found, and so it never moved to the next page. When I changed the search string, suddenly click() was working.
So my troubleshooting turned out to be barking up the wrong tree. But, still, I did learn a few things from it.

Using evaluate() With jQuery

Instead of calling this.click() you can do something like this:
this.evaluate(function(){
  $('#myButton').click();
  };
This runs JavaScript from within the browser’s context. In this case I use jQuery. This works just as well as using Casper’s click() outside the evaluate().
Here there is no advantage. But, by being in a different scope, we have extra flexibility: we could call other functions, or add new event handlers, etc, etc.

Using evaluate() With d3

Here was another of my attempts, but this one does not work:
this.evaluate(function(){
  d3.select('#myButton').click();
  };
The reason: a d3 selection does not have a ready-made click() function.

Making events happen

this.evaluate(function(){
  var evt = document.createEvent("MouseEvents");
  evt.initMouseEvent("click", true, true, window,
    0, 0, 0, 0, 0, false, false, false, false, 0, null);
  return d3.select('#myButton').node().dispatchEvent(evt);
  };
This is how you do a click with d3 (you could use this approach with jQuery too (see a helper function), but are unlikely to ever need to). The first lines create a (simulated) mouse click. The final line sends that event to the DOM item of interest.
Aside: dispatchEvent() returns false if any of the event handlers
cancelled it, true otherwise.
I learned this here; that answer also says this should have worked:
this.evaluate(function(){
  d3.select('#myButton').on("click")()
  };
This definitely does not work for me. Why? It is actually a cheat, trying to find and call the click handler for the button. In my case the click handler was attached to the parent object (a <g>) not the object I was calling select on. (Also, because it is a cheat, this approach does not work when you’ve attached multiple handlers.)
By the way, if not using jQuery or d3, you can use querySelector() and do it this way:
this.evaluate(function(){
  var evt = document.createEvent("MouseEvents");
  evt.initMouseEvent("click", true, true, window,
    0, 0, 0, 0, 0, false, false, false, false, 0, null);
  return document.querySelector('#myButton').dispatchEvent(evt);
  };

Summary

The real lesson here, for me, was when something doesn’t work, make sure you are judging success in the right way!
Beyond that, it turns out there are a whole host of ways to click a button in a CasperJS script. Use the simplest when you can, bear the others in mind for special occasions.
Written with StackEdit.

Thursday, June 24, 2010

Zend Form: display group and custom decorator

I keep meaning to write a proper review of Zend Framework, but I am waiting to finish a big project that uses it. It is running late, and I think ZF can take some of the blame. So don't hold your breath waiting for a positive review.

Today's topic: I want to have part of my form hidden initially, and instead show a button saying: "toggle more questions".

So, we make it a display group (where ex1 and ex2 are the form elements to hide):
$form->addDisplayGroup(array('ex1','ex2'),'extras');

By default this wraps it in a fieldset, with no place I could see to slip in my CSS, javascript and the toggle link. What I need is a decorator, I said to myself.

Oh, gasp, does Zend make this difficult or what! In another part of this project custom decorators had been used for a minor layout change. 7 classes in 3 directories, almost all of it boilerplate. The real work was being done in CSS; those 7 classes were just to give a way to name the items as far as I could tell.

The problem is the Zend Framework Philosophy of making things complex. Did you realize there is no addDecorator($myclass) function! You have to keep decorators in a special directory and then tell Zend where to get them. Then addDecorator('part_of_my_class_name').

ZF's saving grace is that it is open source. So I poked around, and here is my solution. First, I'll define an alias for readability (optional):
$displayGroup=$form->getDisplayGroup('extras');

Remove all the default stuff I don't need (this step is optional too):
$displayGroup->removeDecorator('Fieldset');
  $displayGroup->removeDecorator('HtmlTag');
  $displayGroup->removeDecorator('DtDdWrapper');

Now insert my decorator (these 4 lines are in lieu of addDecorator($myclass)):
$decorators=$displayGroup->getDecorators();
  $decorators['MyTest']=new MyTestDecorator();
  $displayGroup->clearDecorators();
  $displayGroup->addDecorators($decorators);

(I.e. get the current decorators, add mine, then replace the existing decorators with the new set.)


Finally we get to the meat. All you need is to define a render() function that takes a string (the existing content) and returns that string, optionally modified. Here is the minimal version that does nothing.
class MyTestDecorator extends Zend_Form_Decorator_Abstract
{
  public function render($content){ return $content; }
}

And here is the full version: it hides the elements in the group and uses JQuery to show/hide it. The CSS is inline.
class MyTestDecorator extends Zend_Form_Decorator_Abstract
{
  public function render($content)
  {
    $js="$('#extra_questions').toggle();return false;";
    return '<a href="" onclick="'.$js.'">Toggle Tags Visibility</a>'.
      '<div id="extra_questions" style="display: none;">'.$content.'</div>';
  }
}

Yep, it's that simple. Oooh, the architects of ZF must be turning red with rage ;-)

UPDATE: The best article I've found so far on Zend Form Decorators. As many of the comments say, the length of the article is also a very good argument against using Zend Form. And it still didn't answer my questions, so it really needed to be 2-3 times as long. But if you need to format a form, it is a far more useful resource than the utterly inadequate Zend Form manual.

Friday, June 11, 2010

jquery: dcookorg_annotator V0.3 released

I know, I'm behind in my jquery plugin announcements, so I'm going to do three in one.

First up is annotator, for annotating an image (or anything):
http://dcook.org/software/jquery/annotator/

You can have any number of annotations, can drag them anywhere, and can resize them. In the default mode each annotation has a text box appear underneath it for adding a comment.

New in version 0.3, and shown in the screenshot to the left, are some hooks for attaching a form to each annotation, so you can create a custom form for each one (or any other idea you have)!

MIT-license open-source, and tested in all of IE6, IE7, IE8, Safari, Firefox 3 and Firefox 3.5.




Next up is selector_aspect, which is for selecting part of an image while maintaining a fixed aspect ratio. E.g. useful for cropping an image.
http://dcook.org/software/jquery/selector_aspect/

A simple straightforward plug-in, with not many options.

Third is get_percentage_position, which is used to get the size of one div (or any DOM position) in terms of another div (or any DOM object), and also to get the relative position in the same terms. Not very glamorous, but useful in conjunction with the selector_aspect plugin, for instance. It is available here:
http://dcook.org/software/jquery/get_percentage_position/

Finally, a reminder that my first jquery plugin, to run a magnifier over an image is introduced here:
http://darrendev.blogspot.com/2010/04/jquery-plugin-image-magnifier.html

and available here:
http://dcook.org/software/jquery/magnifier/

and all my jquery plugins are being kept here:
http://dcook.org/software/jquery/

Monday, May 31, 2010

Rotating in jquery (firefox problems)

In my entry on rotating images in jquery/javascript I mentioned I'd not had the problem that the unofficial patch was supposed to fix. Well, now I've seen it even using that patch. The problem is this: if you try to rotate an image (in Firefox, at least) that hasn't fully loaded it all goes wrong.

When rotating in Firefox/Safari you are making a copy of the image; the jquery-rotate patch is to make sure your copy has been properly initialized before doing anything with it. My problem was similar: I was trying to rotate an image that hadn't loaded. (Curiously it didn't happen on a 106Kb image, but did consistently on a 69Kb image; the smaller image was in landscape, while the larger image was portrait, but I have no idea if that is related.)

My rotate call is in a function called init(). I first tried calling init() from the image's onLoad(), which worked most of the time. I also tried a more complex solution that involved not calling init() until both the image's onLoad() and JQuery's $(function(){...}); (what I personally like to call onDomLoaded) had both run. But still it happened on certain images, and I now feel that that level of complexity is not needed (also, onDomLoaded always seems to run before the image's onLoad triggers).

So, my solution is this:
  <img src="test.jpg" id="img"
  onLoad="window.setTimeout('init()',40)">

Using a time-out of 25ms was not enough, but 40ms seems reliable. The downside is that the image flashes on screen briefly in the original orientation before rotate can kick in. We can fix that by having it invisible initially:
  <img src="test.jpg" id="img" style="visibility:hidden"
  onLoad="window.setTimeout('init()',40)">

  <script>
  function init(){
    $('#img').rotate(90);
    $('#img').css('visibility','visible');
    }
  </script>

Monday, May 24, 2010

Rotating in jquery (and IE8 problems)

Rotating an image in jquery is harder than you might think. Harder than I had imagined it would be, at least. The first problem is that it is not part of either JQuery or JQueryUI, so you are out there in the wilderness where the 3rd party plugins live. And, trust me, it can get wild out there.

I first tried jqueryrotate. It is quite heavy, at around 10KB, but the main reason I abandoned it is that it didn't work properly for me (sorry, I cannot even remember why now).

Next I tried jquery-rotate and I soon got this working in Firefox 3. My code also worked in Safari and Firefox 3.6 first time. IE6/7/8 were the problem. Under the hood all these plugins use DXImageTransform.Microsoft.Matrix for the IE browsers (which works back to IE5 apparently), and Canvas for all other browsers (which works from at least Firefox 3). They even allow rotation of any angle, though I only needed to rotate in steps of 90 degrees.

(By the way jquery-rotate seems to be unmaintained, so I'm using this unofficial patch even though my testing didn't see the problems it fixes.)

Here is my code:
  $('#img').rotate(rotation,true);

Then:
  if(rotation==90 || rotation==270){
    $('#img').width(300);
    $('#img').height(225);
    }
  else{
    $('#img').width(300);
    $('#img').height(400);
    }

(To keep that code sample clearer I've hard-coded the sizes, to assume an image that has a 3:4 aspect ratio in its original position). The point of this code is to scale the image down to fit in the layout. This was where the first cross-browser problem appears. When Firefox/Safari rotate it they are creating a new object of the new dimensions. As far as I can tell when IE rotate it they are creating an optical illusion. It appears to have been rotated but when you read its width/height you get the numbers for the original position; and the same when you try to set them. (As an aside I tried a number of ideas to get around this underlying issue, but all ended in failure, so I guess it is just the way IE works.)

Tinaysh... Tenacish... Tenaciousness is my middle name, even if it is hard to spell. This code does the job:
  if(rotation==90 || rotation==270){
    if(document.all && !window.opera){  //IE-specific
        $('#img').width(225);
        $('#img').height(300);
        }
    else{
        $('#img').width(300);
        $('#img').height(225);
        }
    }
  else{
    $('#img').width(300);
    $('#img').height(400);
    }

OK, images rotate nicely in all browsers, on to the Next Problem.

I attach a draggable and resizable div (see the demos at http://dcook.org/software/jquery/magnifier/ to get an idea) to the image. I don't want it to leave the image so I use this:
mydiv.draggable({containment:img});

When rotation is 0 or 180 everything is fine, but rotations of 90 and 270 go wrong in IE6/IE7/IE8; it seems the different coordinate system confuse it and I can move mydiv outside the image. (Incidentally jquery's resizable containment is broken, so I had to hand-code that; my resizable containment code is not affected by this problem!)

But things get worse. In IE8 *only*, the rotated image overlaps the following page items; IE6/IE7 correctly push the page down when a landscape image gets rotated to become a portrait image. I could have lived with containment not working, but this one is a showstopper.

We can put the image in a named div, then alter the width/height of that div each time we rotate. So the code ends up as this:
  if(rotation==90 || rotation==270){
    if(document.all && !window.opera){  //IE-specific
        $('#img').width(225);
        $('#img').height(300);
        $('#img_outer').width(300);
        $('#img_outer').height(225);
        }
    else{
        $('#img').width(300);
        $('#img').height(225);
        }
    }
  else{
    $('#img').width(300);
    $('#img').height(400);
    if(document.all && !window.opera){  //IE-specific
        $('#img_outer').width(300);
        $('#img_outer').height(400);
        }
    }
You can also fix the other problem by using #img_outer as the containment div, instead of #img. That fixes all problems in IE6/IE7 (though be careful with margins, padding and borders; i.e. make sure #img_outer is exactly the same size as #img).

IE8 is still less than ideal. Using #img_outer stops it overlapping with the following content, but it puts hard white space in the area where the image would be if height/width were reversed (and the draggable div can still be dragged into that area). For a landscape image that has been rotated that means whitespace to the right of the image, messing up multi-column table layouts. For a portrait image that has been rotated that means a lump of whitespace between the bottom of the image and the start of the next page content. Using overlap:hidden did not help.

This is very novel: a bug in IE8 that isn't in any other browser, not even IE6! I've not solved this, and welcome advice. Yes, okay, I could use absolute positioning to put a "Download Firefox" icon in that white area, but that isn't quite the advice I'm looking for...

Thursday, April 22, 2010

jquery, click here to crash IE8

IE6/7/8 had me pulling me hair out again, but I've just found the problem!

Here is the stripped-down code that shows the problem. We have a few divs, one that is active (and can be resized and dragged). The others are inactive but can be clicked to turn them into the active item.

var currentItem=null;

function makeActive(item){
if(currentItem){
    currentitem.css({borderWidth:1,zIndex:998})
        .draggable('disable')
        .resizable('disable')
        .click(function(){makeActive($(this));});
        ;
    }
item.css({borderWidth:3,zIndex:999})
    .draggable('enable')
    .resizable('enable')
    ;
currentItem=item;
}

Clicking back and forth between two items was fine in firefox, but IE8 would lock up (IE6/IE7 were the same). I kept stripping it down until it clicked (pun intended, sorry!). Yes, on each loop the click handler is added again. Each click handler is calling this function recursively and I think that is what locking up IE8.

The solution is simply to change the .click line to look like this:
   .one('click',function(){makeActive($(this));});

That doesn't just make IE8 happy, it is also more clearly describes the click handler we want. In fact now I understand the problem I'm surprised firefox was not crashing too.

P.S. resizable('disable') does not work in JQuery 1.7; it is apparently fixed in JQuery 1.8 though.

Monday, April 19, 2010

jquery plugin: image magnifier

I've just released my first fully-fledged and useful jquery plugin:

     http://dcook.org/software/jquery/magnifier/

It allows you to magnify an image and examine just one part of it.
Handles on the edge of the "magnifying glass" allow resizing it, which alters the degree of magnification.
The plugin is fully documented, with numerous usage examples.
It runs on all major browsers and operating systems.
Naturally it is open source (MIT).

Thursday, April 8, 2010

Jquery cheat sheets

A cheat sheet, printed out, can be invaluable. Here are a few I've looked at:

My choice:
http://www.javascripttoolbox.com/jquery/cheatsheet/

Nice and compact, going into good details. It comes in colour, but the wonderful thing is it also comes in Excel format, so I could edit the colours.
It doesn't show the new in 1.3 functions, and doesn't point out the deprecated ones ($.browser and $.boxmodel); I annotated that myself using the "Too Colourful" one below.


Too new:
http://labs.impulsestudios.ca/jquery-cheat-sheet

This one is nice, fits on one page, already monochrome (though using a light grey for optional parameters, which is hard to read). Does not show the parameters like the my first choice above. It is for jquery 1.4, showing what is new for 1.4; but I also wanted to know what to avoid if I wanted to write a plugin that would work back to 1.2.


Too Colourful:
http://www.artzstudio.com/files/jquery-rules/jquery_1.3_cheatsheet_v1.pdf
2 pages worth, shows what is new in jquery 1.3, and what is deprecated. Uses colour and looked terrible as a b/w print-out. Also, it lists each version of similar functions (such as event handlers), which is why it needs two pages instead of one.