Tuesday, 11 August 2015

Competition: Creating an application just for fun

When you're a developer and you like what you are doing, at some moment in time you will create something just for fun.

In 2012 I found myself in such a situation. I was playing with my friends to guess scores from Premier League. Till that time we kept an excel file with all the users, their scores and we had a special column to compute a total for all games in a stage (1 point for guessed prognostic and 3 points for exactly guessed score). That file was kept by one of us, sent by mail to all to complete scores, then sent back, and the admin user was updating the main file and so on. This process became soon a pain.

I started to create a web application to make it easier for all of us. Having an online web application with user security where users log and complete scores, then every computation is automatically done, was the right thing to do.

I did not want just a simple competition, like our Premier League. So I thought at it in a general manner. Administrator should be able to create a competition with a number of teams, stages and games, then a number of users will be registered to it. When a stage is approaching, users that did not complete it will be notified by mail to do so. In the day of the stage, application will inquire a site for live scores and, when finish, the scores will be automatically saved and a process of points computation will be run.

I chose as user scoring a simple algorithm to give 1 point for guessed prognostic, 3 points for guessed score when were at most 3 goals, n points for guessed score were n > 3 was the number of goals. This decision brings benefit to those that will guess a 2-2, 3-2 , or any score with more than 3 goals. To make it even more interesting, I created a section of questions for a competition. If user guesses a response he will be rewarded with 3 points. In time, another feature was added: a playoff. A competition can have a playoff phase. Starting from a defined stage, users will compete one against other and the user with a bigger scoring in that stage will advance to next round. The winner of the playoff will get a bonus of 10 points.

Using Java, Wicket and Spring I started to make this application happen. Spring Security helped to achieve users login/logout feature. When users are not logged , they can only see the real scores, but cannot see any data related to a user. When users are logged, they can see their scores, and after the stage started, they can see others scores.

Main screen is just a list of competitions (with Top3 aside) which by default shows only 4 of them. To look for other competitions, a search feature is used.


For small window sizes (like on mobile phones) the main screen is adjusted accordingly:



Admin user will be the only one that will see a toolbar inside main screen. From here, he can create users, competitions, teams, stages, games, questions. Also, admin can manually run the computation process , can reset top trends, can import a competition from a text file and can see the logins history.

By clicking a competition, a new screen will show all data for that competition:


At top, an auto-scroll banner will show the scores for last stage. User can see any stage from that competition and can enter scores for any stage that did not start yet. When the scores are automatically entered and after the computation process, user will see the number of points gained for every game and a total for that stage. User can also see how all other users performed. A Top section will show the total points for that competition with trend arrows (who climbed, who went down). There are a lot of other sections like the top of the real teams from that competition, the playoff, a rss feed to find news about that competition, statistics, graphics, questions.

Inside Statistics section, a general report for entire competition can be generated for every user. User can also see who was his lucky team because it brought him the most points. Also average points per stage top and the best stage scoring can be seen here:


Graphics can show user points evolution and some "awards" like who is the luckiest, who is the best at home games, who is the best at away games and who is the best at draw games.


All reports and charts were created and run using NextReports . I also made the code available on github.

There are more than 3 years from it's inception and many editions of Premier League, UEFA Champions League, World Cup, European Championship were finished. Competition fulfilled our needs: very easy to manage, to enter scores, notifications, auto computation processes. At the end all we need to do is to buy a cup for the winner and to celebrate him. Hurray!

Tuesday, 17 February 2015

Capture HTML elements to PDF

Capture HTML elements to a PDF file is an interesting feature some projects may need. My need was to capture a dashboard full of widgets.

For this I used two javascript libraries:
- html2canvas which is able to capture DOM elements as images
- jspdf which is able to create PDF documents in javascript

The ideea behind can be resumed to a small code snippet:

var doc = new jsPDF('l', 'mm'); 

html2canvas($(elementId), {
          onrendered: function(canvas) {         
              var imgData = canvas.toDataURL('image/png'); 
              ....
              doc.addImage(imgData, 'PNG', x, y);
              ...
              doc.save('dashboard.pdf');
           }
}

We can search for the DOM element with a specific id, capture it as a png image and add it to a landscape PDF file, in this case with a unit measure in mm.

I added this feature to NextReports Charts library with an example in main-test.html.



Here I tested for creating a pdf file with the full dashboard on a landscape page and for creating a pdf file with every widget on a different page.

In first case I do the following:

            var list = $('#dashboard');
            var pdfSettings = new Object();
            pdfSettings.doc = doc;
            pdfSettings.elements = list;
            pdfSettings.title = "NextReports Dashboard";
            capturePdf(pdfSettings);

This will create a nice PDF page:


In second case I look for all canvas elements in my HTML:

            var list = $('canvas[id^="canvas"]');           
            var pdfSettings = new Object();
            pdfSettings.doc = doc;
            pdfSettings.elements = list;
            pdfSettings.position = 1;
            pdfSettings.title = "NextReports Dashboard";
            pdfSettings.showFooter = true;    
            capturePdf(pdfSettings); 

By default a footer message will show the current page:


Footer message can be modified by:

pdfSettings.footerText = "Generated by NextReports"; 

We need to specify the position as 1 because the method is recursive and position is incremented for every new page.

This solution has a drawback. html2canvas library can take a snapshot of what is visible, if there is a scroll involved and some components are not visible, they will not be captured. NextReports Server uses this pdf capture for dashboard elements. To make an entire dashboard visible inside PDF capture, users can play with application and browser features:

- NextReports Server has buttons to hide some components like dashboards explorer
- Browser F11 button makes it to be visible in full screen
- CTRL+mouse wheel will allow to zoom-in/zoom-out the browser content

Using such tricks we may be able to make our dashboard to fit the visible area and PDF capture to look good.

Thursday, 18 September 2014

Java Versions War

Java is portable, so you can run your Java applications on any operating system which supports Java.

But in our days this is not enough. Your clients may have a Java version like 1.6, 1.7 and even 1.8. We do not talk here about old versions prior to Java 1.6. Major changes in Java language were done in 1.7 and 1.8 versions bringing some unseen problems to your applications.

This is the case if your applications depend on many third-party libraries and frameworks. Why? Because your libraries and frameworks may be old and you need to update them. And sometimes, updating a framework is not as easy as changing the version number inside your maven pom file. Changing a framework may involve changing server version, other libraries and many times changing even java code.

So it is not as easy as it seems to make your applications work on all Java versions. First you have a minimum Java version specified to users. This is ok, it is a requirement. But you should make your application runnable with all versions which are equal or bigger to minimum version.

When a new Java version appears, you should check if your application can run on it! If it cannot, you should make it to! Otherwise, you will be informed by your clients and "your image" will not be the same to them.

Let's take some small examples.

1. You have a Java 1.6 application server /client  and you want to offer an Web Service to access a JDBC connection through it. You create a class:
public class Connection implements java.sql.Connection

and you implement your methods. Someone who uses Java 1.7 and your web service client will tell you he cannot use your web service. Why? Because in Java 1.7 Connection class has more methods and these are not implemented by your web service. In this case, your client must use Java 1.6 or you should upgrade your application to Java 1.7 and implement new methods.

2. You have a third-party library in your application. For example xstream 1.3.1  it gives an error with Java 7. So you will find out you need to use a version bigger than 1.4.

3. Someone has already Java 1.8. Your server uses AOP with aspectj library and you have a 1.5 aspectj version. He tells you the server does not start because an error is raised and the aspects cannot be weaved. You will find that aspectj library you have does not work with Java 1.8 and you must upgrade it to 1.7 version.

What do you must understand from this? Do not take for granted that if your application works with Java x version it will work automatically and flawlessly with any Java version y > x.

Thursday, 28 August 2014

Using docx4j to generate docx files

When it comes to generate a docx file in Java, there are not so many alternatives. Some are not free and some have a small set of features.

One Java library that offers a good set of features is docx4j. This library uses JAXB to convert between xml and Java objects. I played with it and I managed to create a docx file with needed functionality.

Microsoft docx uses a special measure unit called dxa which represents 1/20 from a point. You should use following conversions:

20 dxa = 1 point
1440 dxa = 1 inch = 72 points

In Java we need to know screen resolution and we can convert from pixels to dxa:

    // get dots per inch
    protected static int getDPI() {
          return GraphicsEnvironment.isHeadless() ? 96 :
                    Toolkit.getDefaultToolkit().getScreenResolution();
    }

    private int pixelsToDxa(int pixels) {
        return  ( 1440 * pixels / getDPI() );         
    }   

We initialize our docx generator:

    private static WordprocessingMLPackage wordMLPackage;
    private static ObjectFactory factory;
    .....
    boolean landscape = false;
    wordMLPackage = WordprocessingMLPackage.createPackage(PageSizePaper.A4, landscape);
    factory = Context.getWmlObjectFactory();    

We can set document page margins to 50 pixels:

    private void setPageMargins() {        
        try {
            Body body = wordMLPackage.getMainDocumentPart().getContents().getBody();
            PageDimensions page = new PageDimensions();
            PgMar pgMar = page.getPgMar();                  
            pgMar.setBottom(BigInteger.valueOf(pixelsToDxa(50)));
            pgMar.setTop(BigInteger.valueOf(pixelsToDxa(50)));
            pgMar.setLeft(BigInteger.valueOf(pixelsToDxa(50)));
            pgMar.setRight(BigInteger.valueOf(pixelsToDxa(50)));            
            SectPr sectPr = factory.createSectPr();   
            body.setSectPr(sectPr);                           
            sectPr.setPgMar(pgMar);  
        } catch (Docx4JException e) {            
            e.printStackTrace();
        }        
    }

We add a table and save the file:

    Tbl table = createTableWithContent();      
    wordMLPackage.getMainDocumentPart().addObject(table); 

    wordMLPackage.save(new File("C:/Test.docx"));

We define a DocxStyle bean class to be used with following properties:

    private boolean bold;
    private boolean italic;
    private boolean underline;
    private String fontSize;
    private String fontColor; 
    private String fontFamily;
    
    // cell margins
    private int left;
    private int bottom;
    private int top;
    private int right;
    
    private String background;
    private STVerticalJc verticalAlignment;
    private JcEnumeration horizAlignment;
    
    private boolean borderLeft;
    private boolean borderRight;
    private boolean borderTop;
    private boolean borderBottom;
    private boolean noWrap;

Our createTableWithContent method follows (we use dxa values). There are 4 rows, some cells with vertical merge, and some with horizontal merge.

    private Tbl createTableWithContent() {

        Tbl table = factory.createTbl();                

        // for TEST: this adds borders to all cells
        TblPr tblPr = new TblPr();
        TblStyle tblStyle = new TblStyle();
        tblStyle.setVal("TableGrid");
        tblPr.setTblStyle(tblStyle);
        table.setTblPr(tblPr);

        Tr tableRow = factory.createTr();        

        // a default table cell style
        DocxStyle defStyle = new DocxStyle();
        defStyle.setBold(false);
        defStyle.setItalic(false);
        defStyle.setUnderline(false);
        defStyle.setHorizAlignment(JcEnumeration.CENTER);

        // a specific table cell style 
        DocxStyle style = new DocxStyle();
        style.setBold(true);
        style.setItalic(true);
        style.setUnderline(true);
        style.setFontSize("40");
        style.setFontColor("FF0000");
        style.setFontFamily("Book Antiqua");
        style.setTop(300);
        style.setBackground("CCFFCC");        
        style.setVerticalAlignment(STVerticalJc.CENTER);
        style.setHorizAlignment(JcEnumeration.CENTER);
        style.setBorderTop(true);
        style.setBorderBottom(true);
        style.setNoWrap(true);

        addTableCell(tableRow, "Field 1", 3500, style, 1, null);
        // start vertical merge for Filed 2 and Field 3 on 3 rows
        addTableCell(tableRow, "Field 2", 3500, defStyle, 1, "restart");
        addTableCell(tableRow, "Field 3", 1500, defStyle, 1, "restart");        
        table.getContent().add(tableRow);        

        tableRow = factory.createTr();
        addTableCell(tableRow, "Text", 3500, defStyle, 1, null);
        addTableCell(tableRow, "", 3500, defStyle, 1, "");
        addTableCell(tableRow, "", 1500, defStyle, 1, "");
        table.getContent().add(tableRow);

        tableRow = factory.createTr();
        addTableCell(tableRow, "Interval", 3500, defStyle, 1, null);
        addTableCell(tableRow, "", 3500, defStyle, 1, "close");
        addTableCell(tableRow, "", 1500, defStyle, 1, "close");
        table.getContent().add(tableRow);                          

        // add an image horizontally merged on 3 cells
        String filenameHint = null;
        String altText = null;
        int id1 = 0;
        int id2 = 1; 
        byte[] bytes = getImageBytes();
        P pImage;
        try {
            pImage = newImage(wordMLPackage, bytes, filenameHint, altText, id1, id2, 8500);
            tableRow = factory.createTr();
            addTableCell(tableRow, pImage, 8500, defStyle, 3, null);
            table.getContent().add(tableRow);
        } catch (Exception e) {
            e.printStackTrace();
        }
 
        return table;
    }


The most important method is addTableCell with two signatures, one for an image and one for a text cell:
    private void addTableCell(Tr tableRow, P image, int width, DocxStyle style, 
                        int horizontalMergedCells, String verticalMergedVal) {
        Tc tableCell = factory.createTc();                
        addImageCellStyle(tableCell, image, style);                
        setCellWidth(tableCell, width);                
        setCellVMerge(tableCell, verticalMergedVal);                
        setCellHMerge(tableCell, horizontalMergedCells);                            
        tableRow.getContent().add(tableCell);
    }
    
    private void addTableCell(Tr tableRow, String content, int width,
               DocxStyle style, int horizontalMergedCells, String verticalMergedVal) {
        Tc tableCell = factory.createTc();                
        addCellStyle(tableCell, content, style);                 
        setCellWidth(tableCell, width);                
        setCellVMerge(tableCell, verticalMergedVal);                
        setCellHMerge(tableCell, horizontalMergedCells);
        if (style.isNoWrap()) {
            setCellNoWrap(tableCell);
        }        
        tableRow.getContent().add(tableCell);
    } 

Then we look at addCellStyle method:
 
    private void addCellStyle(Tc tableCell, String content, DocxStyle style) {
        if (style != null) {
                                   
            P paragraph = factory.createP();
    
            Text text = factory.createText();
            text.setValue(content);
    
            R run = factory.createR();
            run.getContent().add(text);
    
            paragraph.getContent().add(run);
            
            setHorizontalAlignment(paragraph, style.getHorizAlignment());
                    
            RPr runProperties = factory.createRPr();
            
            if (style.isBold()) {
                addBoldStyle(runProperties);
            }
            if (style.isItalic()) {
                addItalicStyle(runProperties);
            }
            if (style.isUnderline()) {
                addUnderlineStyle(runProperties);
            }
            
            setFontSize(runProperties, style.getFontSize());                
            setFontColor(runProperties, style.getFontColor());                
            setFontFamily(runProperties, style.getFontFamily());
                    
            setCellMargins(tableCell, style.getTop(), style.getRight(), 
                           style.getBottom(), style.getLeft());
            setCellColor(tableCell, style.getBackground());
            setVerticalAlignment(tableCell, style.getVerticalAlignment());
            
            setCellBorders(tableCell, style.isBorderTop(), style.isBorderRight(), 
                           style.isBorderBottom(), style.isBorderLeft());
    
            run.setRPr(runProperties);
    
            tableCell.getContent().add(paragraph);
        }
    }
and the same method for image:
    private void addImageCellStyle(Tc tableCell, P image, DocxStyle style) {        
        setCellMargins(tableCell, style.getTop(), style.getRight(), 
                       style.getBottom(), style.getLeft());
        setCellColor(tableCell,  style.getBackground());
        setVerticalAlignment(tableCell, style.getVerticalAlignment());        
        setHorizontalAlignment(image, style.getHorizAlignment());
        setCellBorders(tableCell, style.isBorderTop(), style.isBorderRight(), 
                       style.isBorderBottom(), style.isBorderLeft());
        tableCell.getContent().add(image);
    }

To create an image paragraph we will do the following:
     public P newImage(WordprocessingMLPackage wordMLPackage, byte[] bytes, 
                       String filenameHint, String altText, int id1,
            int id2, long cx) throws Exception {
        BinaryPartAbstractImage imagePart = BinaryPartAbstractImage.createImagePart(wordMLPackage, bytes);
        Inline inline = imagePart.createImageInline(filenameHint, altText, id1, id2, cx, false);
        // Now add the inline in w:p/w:r/w:drawing
        ObjectFactory factory = Context.getWmlObjectFactory();
        P p = factory.createP();
        R run = factory.createR();
        p.getContent().add(run);
        Drawing drawing = factory.createDrawing();
        run.getContent().add(drawing);
        drawing.getAnchorOrInline().add(inline);
        return p;
     }

Then we present all methods used to set cell properties (names are self explanatory):

     private void setCellBorders(Tc tableCell, boolean borderTop, boolean borderRight, 
                                 boolean borderBottom, boolean borderLeft) {
                
        TcPr tableCellProperties = tableCell.getTcPr();
        if (tableCellProperties == null) {
            tableCellProperties = new TcPr();
            tableCell.setTcPr(tableCellProperties);
        }
        
        CTBorder border = new CTBorder();
        // border.setColor("auto");
        border.setColor("0000FF");
        border.setSz(new BigInteger("20"));
        border.setSpace(new BigInteger("0"));
        border.setVal(STBorder.SINGLE);            
        
        TcBorders borders = new TcBorders();
        if (borderBottom) {
            borders.setBottom(border);
        } 
        if (borderTop) {
            borders.setTop(border);
        } 
        if (borderLeft) {
            borders.setLeft(border);
        }
        if (borderRight) {
            borders.setRight(border);
        }
        tableCellProperties.setTcBorders(borders);        
    }

    private void setCellWidth(Tc tableCell, int width) {
        if (width > 0) {
            TcPr tableCellProperties = tableCell.getTcPr();
            if (tableCellProperties == null) {
                tableCellProperties = new TcPr();
                tableCell.setTcPr(tableCellProperties);
            }
            TblWidth tableWidth = new TblWidth();
            tableWidth.setType("dxa");
            tableWidth.setW(BigInteger.valueOf(width));
            tableCellProperties.setTcW(tableWidth);
        }
    }
    
    private void setCellNoWrap(Tc tableCell) {
        TcPr tableCellProperties = tableCell.getTcPr();
        if (tableCellProperties == null) {
            tableCellProperties = new TcPr();
            tableCell.setTcPr(tableCellProperties);
        }
        BooleanDefaultTrue b = new BooleanDefaultTrue();
        b.setVal(true);
        tableCellProperties.setNoWrap(b);        
    }

    private void setCellVMerge(Tc tableCell, String mergeVal) {
        if (mergeVal != null) {
            TcPr tableCellProperties = tableCell.getTcPr();
            if (tableCellProperties == null) {
                tableCellProperties = new TcPr();
                tableCell.setTcPr(tableCellProperties);
            }
            VMerge merge = new VMerge();
            if (!"close".equals(mergeVal)) {
                merge.setVal(mergeVal);
            }
            tableCellProperties.setVMerge(merge);
        }
    }
    
    private void setCellHMerge(Tc tableCell, int horizontalMergedCells) {
        if (horizontalMergedCells > 1) {
            TcPr tableCellProperties = tableCell.getTcPr();
            if (tableCellProperties == null) {
                tableCellProperties = new TcPr();
                tableCell.setTcPr(tableCellProperties);
            }
    
            GridSpan gridSpan = new GridSpan();
            gridSpan.setVal(new BigInteger(String.valueOf(horizontalMergedCells)));
    
            tableCellProperties.setGridSpan(gridSpan);
            tableCell.setTcPr(tableCellProperties);
        }                
    }    
    
    private void setCellColor(Tc tableCell, String color) {
        if (color != null) {
            TcPr tableCellProperties = tableCell.getTcPr();
            if (tableCellProperties == null) {
                tableCellProperties = new TcPr();
                tableCell.setTcPr(tableCellProperties);
            }
            CTShd shd = new CTShd();
            shd.setFill(color);
            tableCellProperties.setShd(shd);
        }
    }

    private void setCellMargins(Tc tableCell, int top, int right, int bottom, int left) {
        TcPr tableCellProperties = tableCell.getTcPr();
        if (tableCellProperties == null) {
            tableCellProperties = new TcPr();
            tableCell.setTcPr(tableCellProperties);
        }
        TcMar margins = new TcMar();

        if (bottom > 0) {
            TblWidth bW = new TblWidth();
            bW.setType("dxa");
            bW.setW(BigInteger.valueOf(bottom));
            margins.setBottom(bW);
        }

        if (top  > 0) {
            TblWidth tW = new TblWidth();
            tW.setType("dxa");
            tW.setW(BigInteger.valueOf(top));
            margins.setTop(tW);
        }

        if (left > 0) {
            TblWidth lW = new TblWidth();
            lW.setType("dxa");
            lW.setW(BigInteger.valueOf(left));
            margins.setLeft(lW);
        }

        if (right > 0) {
            TblWidth rW = new TblWidth();
            rW.setType("dxa");
            rW.setW(BigInteger.valueOf(right));
            margins.setRight(rW);
        }

        tableCellProperties.setTcMar(margins);
    }

    private void setVerticalAlignment(Tc tableCell, STVerticalJc align) {
        if (align != null) {
            TcPr tableCellProperties = tableCell.getTcPr();
            if (tableCellProperties == null) {
                tableCellProperties = new TcPr();
                tableCell.setTcPr(tableCellProperties);
            }
    
            CTVerticalJc valign = new CTVerticalJc();
            valign.setVal(align);
    
            tableCellProperties.setVAlign(valign);
        }
    } 
 
    private void setFontSize(RPr runProperties, String fontSize) {
       if (fontSize != null && !fontSize.isEmpty()) {
          HpsMeasure size = new HpsMeasure();
          size.setVal(new BigInteger(fontSize));
          runProperties.setSz(size);
          runProperties.setSzCs(size);
       }
    }
 
    private void setFontFamily(RPr runProperties, String fontFamily) {
       if (fontFamily != null) {
          RFonts rf = runProperties.getRFonts();
          if (rf == null) {
             rf = new RFonts();
             runProperties.setRFonts(rf);
          }
          rf.setAscii(fontFamily);
       }
    }
 
    private void setFontColor(RPr runProperties, String color) {
       if (color != null) {
          Color c = new Color();
          c.setVal(color);
          runProperties.setColor(c);
       } 
    }
 
    private void setHorizontalAlignment(P paragraph, JcEnumeration hAlign) {
       if (hAlign != null) {
          PPr pprop = new PPr();
          Jc align = new Jc();
          align.setVal(hAlign);
          pprop.setJc(align);
          paragraph.setPPr(pprop);
       }
    }

    private void addBoldStyle(RPr runProperties) {
       BooleanDefaultTrue b = new BooleanDefaultTrue();
       b.setVal(true);
       runProperties.setB(b);
    }

    private void addItalicStyle(RPr runProperties) {
       BooleanDefaultTrue b = new BooleanDefaultTrue();
       b.setVal(true);
       runProperties.setI(b);
    }

    private void addUnderlineStyle(RPr runProperties) {
       U val = new U();
       val.setVal(UnderlineEnumeration.SINGLE);
       runProperties.setU(val);
    }
As you see it takes some work to generate a simple file. Docx4j is also slow and it's api looks awful for Java programers with short method names and classes, but this is the result of internally using Jaxb mapping. Despite these things, if you need to generate a docx file, you should take Docx4j as a suitable candidate.

Thursday, 19 June 2014

Javascript window resize listeners force you to double check your code

Working to my NextCharts HTML5 library made me to think more defensive when writing Javascript code. NextCharts library is used by NextReports Server inside its Dashboards section. Here, any dashboard can contain a number of charts and widgets.

Any chart / widget has a resize event defined in its code , like :

window.addEventListener('resize', resize, false); 

So, when the browser is resized, all charts and widgets are also resized.

The problems may start to appear when we change between dashboards. Visually I saw, sometimes , that the animation is not working. Looking for Javascript errors, I saw that in some place my canvas element was null. How can it be? I was just doing:

var canvas = document.getElementById(id);

Why is not the element id found?

And then it struck me: The resize event is registered for some charts/widgets and when we change the dashboard those listeners are still active, the browser notifies them about the event, but the components are not found inside the page anymore, so the canvas is null for them.

Because we do not know when to remove the resize listener for a specific component inside dashboard, the solution is to check for canvas null values in our javascript functions:

if (canvas == null) {
      return;
}
This is something you cannot think until you use your Javascript code under a big umbrella.

Friday, 14 February 2014

Get JSON with JQuery as UTF-8

This is how I used JQuery, to read JSON from a url:

$.getJSON('data-html5.json').then( 
    function(data){  
        console.log(data);
        nextChart(data, 'canvas', 'tip');
    },
    function(jqXHR, textStatus, errorThrown) {
        alert("Error: " + textStatus + " errorThrown: " + errorThrown);
    } 
);

But this method does not interpret UTF-8 characters, making your UTF-8 text to look scrambled. The workaround is to use the following:

$.ajax({
    type: "POST",
    url: "data-html5.json",
    contentType: "application/json; charset=utf-8",
    dataType: "json",    
    success: function(data) {
        console.log(data);
        nextChart(data, 'canvas', 'tip');
    },
    error: function(jqXHR, textStatus, errorThrown) {
        alert("Error: " + textStatus + " errorThrown: " + errorThrown);
    }
});  

Thursday, 14 November 2013

HTML Multiple select and Android incompatibilities

Do you have a multiple select on your web site? Then you should see how ugly it looks on Android platform. Not only it looks different, but it also behaves differently.

In Wicket there is a Palette component (two multiple selects) which allows to select some options from a list to another list. You can add / remove selected options using two buttons ">" and "<".


I extended this component with some new buttons ">>" and "<<" to add all and remove all elements.

On Android platform:
  • the multiple select looks like a simple select without any option in it
  • select does not even has an arrow to indicate you are looking at a list
  • even if you make its height bigger you cannot see all the possible options inside a multiple select; (it is just an empty big space!)
  • when you click on it you can select what options you want
  • after selection you can see only the first selected option
Because all of these, there is also no need for "add all" and "remove all" buttons (even if it does the operation, not being able to see what are all options makes it unusable).

When I first re-sized the browser to become with less than 480 px width, I saw the result ok . I just hid "add all" and "remove all" buttons.


But when I looked on Android emulator I saw another story.


First I modified the css to show the two buttons inline.  The re-sized browser looks like:


And then I added the arrow icon by myself (it will have impact only on Android):

.paletteContainer table.palette td.pane select,
.paletteContainer table.palette td.pane select[size="0"],
.paletteContainer table.palette td.pane select[size="1"] {
    background-image: url(data:image/png;base64,R0lGODlhDQAEAIAAAAAAAP8A
/yH5BAEHAAEALAAAAAANAAQAAAILhA+hG5jMDpxvhgIAOw==);
    background-repeat: no-repeat;
    background-position: right center;
    padding-right: 1.8em;
  }

The result was much better:


When you click on select you see Android selection dialog:


After selecting, the first selected option is seen: