Tuesday, April 30, 2013

ZK MVVM: Show Validation Message in Modal Window



Simple Note

Introduction

This article describe how to display validation message in a modal window, this is a custom way to simulate the EE only feature so just simple note.

NOTE: It is better to buy a ZK EE License and use the official feature for better stability.

The Result

View demo on line
http://screencast.com/t/zuHrho5u3

The Program

MVVM_Show_Validation_Message_in_Modal_Window.zul
https://github.com/benbai123/ZK_Practice/blob/master/Pattern/MVVM/AdvancedMVVM/WebContent/MVVM_Show_Validation_Message_in_Modal_Window.zul

ShowMessageInModalWinTestVM.java
https://github.com/benbai123/ZK_Practice/blob/master/Pattern/MVVM/AdvancedMVVM/src/blog/ben/test/mvvm/formbinding/ShowMessageInModalWinTestVM.java

Reference

Related thread on forum
http://forum.zkoss.org/question/86565/mvvm-form-binding-validator/

AbstractValidator.java
https://github.com/zkoss/zk/blob/v6.5.1.1/zkbind/src/org/zkoss/bind/validator/AbstractValidator.java#L67

Friday, April 26, 2013

ZK MVVM: Form Binding with Validator


Introduction

This article describe how to use Validator with Form Binding in ZK MVVM.

The Result

View demo on line
http://screencast.com/t/caPtmte73J

Pre-request

ZK MVVM: Form Binding
http://ben-bai.blogspot.tw/2013/04/zk-mvvm-form-binding.html

The Program

MVVM_FormBinding.zul

The inner div bind to vm.person, i.e., use vm.person as a form object. It will save form content before savePerson command and use vm.formValidator as the validator to do validation of form fields.

The validator will store error message in validationMessages map with the specified id 'vmsgs'

<zk>
    <!-- 
        tested with ZK 6.0.2

        validationMessages="@id('vmsgs')"
        means all validation message will be stored in a map
        that mapped by the id 'vmsgs'

        i.e., you can get messages from that map by 
        @bind(vmsgs['keyOfMessageContent'])
    -->
    <div apply="org.zkoss.bind.BindComposer"
        viewModel="@id('vm') @init('blog.ben.test.mvvm.formbinding.FormBindingWithValidatorTestVM')"
        validationMessages="@id('vmsgs')">
        <!-- 
            @validator(vm.formValidator)
            do validation before save form with this validator
         -->
        <div form="@id('fx') @load(vm.person) @save(vm.person, before='savePerson') @validator(vm.formValidator)">
            <vlayout>
                <hlayout>
                    <label value="First Name" />
                    <textbox value="@bind(fx.firstName)" />
                    <!-- 
                        @bind(vmsgs['firstNameContentError'])
                        show error message of the key 'firstNameContentError' in
                        vmsgs map after form validation as needed
                     -->
                    <label value="@bind(vmsgs['firstNameContentError'])" style="color: red;" />
                    <label value="@bind(vmsgs['firstNameError'])" style="color: red;" />
                </hlayout>
                <hlayout>
                    <label value="Last Name" />
                    <textbox value="@bind(fx.lastName)" />
                    <label value="@bind(vmsgs['lastNameContentError'])" style="color: red;" />
                    <label value="@bind(vmsgs['lastNameError'])" style="color: red;" />
                </hlayout>
                <hlayout>
                    <label value="Age" />
                    <intbox value="@bind(fx.age)" />
                    <label value="@bind(vmsgs['ageContentError'])" style="color: red;" />
                    <label value="@bind(vmsgs['ageTooSmallError'])" style="color: red;" />
                    <label value="@bind(vmsgs['ageTooLargeError'])" style="color: red;" />
                </hlayout>
            </vlayout>
        </div>
        <button label="save" onClick="@command('savePerson')" />
        <label value="@load(vm.personContent)" />
    </div>
</zk>


FormBindingTestVM.java

Simple vm, contains getter, command and a validator

package blog.ben.test.mvvm.formbinding;

import org.zkoss.bind.ValidationContext;
import org.zkoss.bind.Validator;
import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.NotifyChange;
import org.zkoss.bind.validator.AbstractValidator;

/**
 * tested with ZK 6.0.2
 * @author benbai123
 *
 */
public class FormBindingWithValidatorTestVM {
    private Person _person;
    public Person getPerson () {
        if (_person == null) {
            _person = new Person("Ben", "Bai", 123); // fake
        }
        return _person;
    }
    public String getPersonContent () {
        return _person.getFirstName() + " " + _person.getLastName() + ", age = " + _person.getAge();
    }
    /**
     * empty function as a command for
     * save in form binding and update personContent
     */
    @Command
    @NotifyChange("personContent")
    public void savePerson () {
    }
    /**
     * get the validator that will do validation while save
     * @return
     */
    public Validator getFormValidator(){
        return new AbstractValidator() {

            public void validate(ValidationContext ctx) {
                // get value from form context,
                // ctx.getProperties("firstName")[0].getValue() will
                // get the value that bind with @bind(fx.firstName)
                // in zul page
                String firstName = (String)ctx.getProperties("firstName")[0].getValue();
                String lastName = (String)ctx.getProperties("lastName")[0].getValue();
                Integer age = (Integer)ctx.getProperties("age")[0].getValue();
                if (firstName == null || firstName.isEmpty()) {
                    // put error message into validationMessages map
                    addInvalidMessage(ctx, "firstNameContentError", "firstName is required ");
                } else if (firstName.length() < 3) {
                    addInvalidMessage(ctx, "firstNameError", "firstName at least 3 chars ");
                }
                if (lastName == null || lastName.isEmpty()) {
                    addInvalidMessage(ctx, "lastNameContentError", "lastName is required");
                } else if (lastName.length() < 3) {
                    addInvalidMessage(ctx, "lastNameError", "lastName at least 3 chars ");
                }
                if (age == null) {
                    addInvalidMessage(ctx, "ageContentError", "age is required");
                } else {
                    if (age < 0) {
                        addInvalidMessage(ctx, "ageTooSmallError", "age should not be negative");
                    }
                    if (age > 130) {
                        addInvalidMessage(ctx, "ageTooLargeError", "age should smaller than 130");
                    }
                }
            }
        };
    }
}


Person.java

Simple pojo contains data of a person

package blog.ben.test.mvvm.formbinding;

public class Person {
    String _firstName;
    String _lastName;
    Integer _age;
    public Person (String firstName, String lastName, Integer age) {
        _firstName = firstName;
        _lastName = lastName;
        _age = age;
    }
    public void setFirstName (String firstName) {
        _firstName = firstName;
    }
    public void setLastName (String lastName) {
        _lastName = lastName;
    }
    public void setAge (int age) {
        _age = age;
    }
    public String getFirstName () {
        return _firstName;
    }
    public String getLastName () {
        return _lastName;
    }
    public Integer getAge () {
        return _age;
    }
}


Reference

MVVM > Data Binding > Validator
http://books.zkoss.org/wiki/ZK_Developer's_Reference/MVVM/Data_Binding/Validator

Download

MVVM_FormBinding_with_Validator.zul
https://github.com/benbai123/ZK_Practice/blob/master/Pattern/MVVM/AdvancedMVVM/WebContent/MVVM_FormBinding_with_Validator.zul

FormBindingWithValidatorTestVM.java
https://github.com/benbai123/ZK_Practice/blob/master/Pattern/MVVM/AdvancedMVVM/src/blog/ben/test/mvvm/formbinding/FormBindingWithValidatorTestVM.java

Person.java
https://github.com/benbai123/ZK_Practice/blob/master/Pattern/MVVM/AdvancedMVVM/src/blog/ben/test/mvvm/formbinding/Person.java

Demo Flash
https://github.com/benbai123/ZK_Practice/blob/master/demo_src/swf/Pattern/MVVM/MVVM_FormBinding_with_Validator.swf

ZK MVVM: Form Binding


Introduction

This article describe how to do Form Binding in ZK MVVM.

The Result

View demo on line
http://screencast.com/t/yAE1f4wTALNr

Pre-request

ZK Basic MVVM Pattern
http://ben-bai.blogspot.tw/2012/12/zk-basic-mvvm-pattern.html

The Program

MVVM_FormBinding.zul

The inner div bind to vm.person, i.e., use vm.person as a form object. It will save form content before savePerson command.

<zk>
    <!-- 
        tested with ZK 6.0.2
     -->
    <div apply="org.zkoss.bind.BindComposer"
        viewModel="@id('vm') @init('blog.ben.test.mvvm.formbinding.FormBindingTestVM')">
        <!-- form binding,
            @id('fx') @load(vm.person)
            here the id (fx) is mapping to vm.person

            @save(vm.person, before='savePerson')"
            save to vm.person before savePerson command
            the before/after in @save is required,
            you should always indicate when (before/after a command)
            to save form content

            save to vm.person means
            call vm.getPerson().setFirstName() to save firstName
        -->
        <div form="@id('fx') @load(vm.person) @save(vm.person, before='savePerson')">
            <vlayout>
                <!-- fx.firstName, i.e., vm.person.firstName -->
                <hlayout>
                    <label value="First Name" />
                    <textbox value="@bind(fx.firstName)" />
                </hlayout>
                <hlayout>
                    <label value="Last Name" />
                    <textbox value="@bind(fx.lastName)" />
                </hlayout>
                <hlayout>
                    <label value="Age" />
                    <intbox value="@bind(fx.age)" />
                </hlayout>
            </vlayout>
        </div>
        <!-- click this button will execute savePerson() method in vm
            and trigger save action of the form above -->
        <button label="save" onClick="@command('savePerson')" />
        <!-- show content of person,
            will be updated after savePerson command -->
        <label value="@load(vm.personContent)" />
    </div>
</zk>


FormBindingTestVM.java

Simple vm, contains getter and command

package blog.ben.test.mvvm.formbinding;

import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.NotifyChange;
/**
 * tested with ZK 6.0.2
 * @author benbai123
 *
 */
public class FormBindingTestVM {
    private Person _person;
    public Person getPerson () {
        if (_person == null) {
            _person = new Person("Ben", "Bai", 123); // fake
        }
        return _person;
    }
    public String getPersonContent () {
        return _person.getFirstName() + " " + _person.getLastName() + ", age = " + _person.getAge();
    }
    /**
     * empty function as a command for
     * save in form binding and update personContent
     */
    @Command
    @NotifyChange("personContent")
    public void savePerson () {
    }
}


Person.java

Simple pojo contains data of a person

package blog.ben.test.mvvm.formbinding;

public class Person {
    String _firstName;
    String _lastName;
    Integer _age;
    public Person (String firstName, String lastName, Integer age) {
        _firstName = firstName;
        _lastName = lastName;
        _age = age;
    }
    public void setFirstName (String firstName) {
        _firstName = firstName;
    }
    public void setLastName (String lastName) {
        _lastName = lastName;
    }
    public void setAge (int age) {
        _age = age;
    }
    public String getFirstName () {
        return _firstName;
    }
    public String getLastName () {
        return _lastName;
    }
    public Integer getAge () {
        return _age;
    }
}


Reference

MVVM > Data Binding > Form Binding
http://books.zkoss.org/wiki/ZK%20Developer's%20Reference/MVVM/Data%20Binding/Form%20Binding

Download

MVVM_FormBinding.zul
https://github.com/benbai123/ZK_Practice/blob/master/Pattern/MVVM/AdvancedMVVM/WebContent/MVVM_FormBinding.zul

FormBindingTestVM.java
https://github.com/benbai123/ZK_Practice/blob/master/Pattern/MVVM/AdvancedMVVM/src/blog/ben/test/mvvm/formbinding/FormBindingTestVM.java

Person.java
https://github.com/benbai123/ZK_Practice/blob/master/Pattern/MVVM/AdvancedMVVM/src/blog/ben/test/mvvm/formbinding/Person.java

Demo Flash
https://github.com/benbai123/ZK_Practice/blob/master/demo_src/swf/Pattern/MVVM/MVVM_FormBinding.swf

Notify ZUL from HTML



Simpole Note

This article describe how to notify zul from a html button click via ajax request, this obviously not the suggest pattern so just simple note.

Program

notify_zul_from_html.html
https://github.com/benbai123/ZK_Practice/blob/master/Components/projects/Components_Practice/WebContent/notify_zul_from_html.html

notify_zul_from_html.zul
https://github.com/benbai123/ZK_Practice/blob/master/Components/projects/Components_Practice/WebContent/notify_zul_from_html.zul

notify_zul_from_html__notify.zul
https://github.com/benbai123/ZK_Practice/blob/master/Components/projects/Components_Practice/WebContent/notify_zul_from_html__notify.zul

Online demo
http://screencast.com/t/IIg4Eg1XB

Thursday, April 25, 2013

ZK Calendar as Week Picker


Introduction

This article describe how to customize the small calendar as a week picker.

Pre-request

Please check Client Side Programming and The use, apply Attribute  at References section before reading this article

The Program

calendar_as_week_picker.zul

<zk xmlns:w="client">
    <style>
        .custom-selected-node {
            background-color: #99FF99 !important;
        }
    </style>
    <vlayout>
        <label value="selected dates" />
        <textbox rows="7" id="tbx" width="300px" />
    </vlayout>
    <calendar id="cal" use="test.custom.component.WeekPicker">
        <attribute w:name="_markCal"><![CDATA[
            function (opts) {
                // clear old custom-selected-node
                jq('.custom-selected-node').each(function () {
                    jq(this).removeClass('custom-selected-node');
                });
                this.$_markCal(opts);
                if (this._view == 'day') {
                    // target: current focused date (td)
                    // parent: tr
                    var target = jq('.z-calendar-seld')[0],
                        parent = target.parentNode,
                        node = parent.firstChild,
                        beforeCnt = 0,
                        found;
                    // loop through each td
                    while (node) {
                        // add selected style
                        jq(node).addClass('custom-selected-node');
                        if (node == target) {
                            found = true;
                        } else if (!found) {
                            // count nodes before target
                            beforeCnt++;
                        }
                        node = node.nextSibling;
                    }
                    // fire event to server
                    this.fire('onCustomSelect', {bcnt: beforeCnt});
                }
            }
        ]]></attribute>
        <attribute name="onCustomSelect"><![CDATA[
            List dates = self.getSelectedDates();
            java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("dd / MM / yyyy");
            String value = "";
            for (int i = 0; i < dates.size(); i++) {
                value = value + sdf.format((Date)dates.get(i)) + "\n";
            }
            tbx.setValue(value);
        ]]></attribute>
    </calendar>
</zk>


WeekPicker.java

package test.custom.component;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;

import org.zkoss.zk.ui.event.Events;
import org.zkoss.zul.Calendar;

public class WeekPicker extends Calendar {
    private static final long serialVersionUID = 7513083343273393743L;
    private List<Date> _selectedDates;
    static {
        addClientEvent(WeekPicker.class, "onCustomSelect", CE_IMPORTANT|CE_REPEAT_IGNORE|CE_NON_DEFERRABLE);
    }
    public List<Date> getSelectedDates () {
        return _selectedDates;
    }
    private void updateSelectedDates (int beforeCnt) {
        _selectedDates = new ArrayList<Date>();
        java.util.Calendar cal = java.util.Calendar.getInstance();
        // current selected date
        cal.setTime(getValue());
        // move to first day of the week
        cal.add(java.util.Calendar.DATE, (-1*beforeCnt));
        // add seven days to _selectedDates
        for (int i = 0; i < 7; i++) {
            _selectedDates.add(cal.getTime());
            cal.add(java.util.Calendar.DATE, 1);
        }
    }
    public void service(org.zkoss.zk.au.AuRequest request, boolean everError) {
        final String cmd = request.getCommand();
        if (cmd.equals("onCustomSelect")) {
            final Map<String, Object> data = request.getData();
            // get node count before selected date
            final Integer beforeCnt = (Integer)data.get("bcnt");
            // update selected dates
            updateSelectedDates(beforeCnt);
            // post event
            Events.postEvent("onCustomSelect", this, null);
        } else {
            super.service(request, everError);
        }
    }
}


The Result

View demo on line
http://screencast.com/t/N35NM2T98yY

References

Calendar.js
https://github.com/zkoss/zk/blob/master/zul/src/archive/web/js/zul/db/Calendar.js

Calendar.java
https://github.com/zkoss/zk/blob/master/zul/src/org/zkoss/zul/Calendar.java

Client Side Programming
http://books.zkoss.org/wiki/Small_Talks/2010/April/Client_Side_Programming

The use, apply Attribute
http://books.zkoss.org/wiki/ZK_Developer%27s_Guide/Fundamental_ZK/ZK_User_Interface_Markup_Language/ZK_Attributes#The_use.2C_apply_Attribute

Download

calendar_as_week_picker.zul
https://github.com/benbai123/ZK_Practice/blob/master/Components/projects/Components_Practice/WebContent/calendar_as_week_picker.zul

WeekPicker.java
https://github.com/benbai123/ZK_Practice/blob/master/Components/projects/Components_Practice/src/test/custom/component/WeekPicker.java

Demo Flash
https://github.com/benbai123/ZK_Practice/blob/master/Components/demos/calendar_as_week_picker.swf

Sunday, April 21, 2013

ZK Pivottable: Filter Row or Column Header Value


Introduction

This article describe how to use the value of row or column field as filter to filter raw data.

Pre-request


Pass Event to Other Component
http://ben-bai.blogspot.tw/2012/12/pass-event-to-other-component.html


ZK Pivottable: Display Data in ZK Pivottable
http://ben-bai.blogspot.tw/2012/07/zk-pivottable-display-data-in-zk.html

ZK Pivottable: Sync the Open Status of Pivotmodel
http://ben-bai.blogspot.tw/2013/02/zk-pivottable-sync-open-status-of.html

ZK Pivottable: Sync Structure of Pivot Model
http://ben-bai.blogspot.tw/2013/02/zk-pivottable-sync-structure-of-pivot.html

ZK Pivottable: Get Distinct Values of Field
http://ben-bai.blogspot.tw/2013/03/zk-pivottable-get-distinct-values-of.html

The Program

index.zul

A pivottable and a filter

<zk>
    <!-- Tested with ZK 6.0.1 CE and ZK Pivottable 2.0.0 -->
    <!-- window, apply a SelectorComposer -->
    <window id="win" xmlns:w="client"
        apply="test.TestComposer">
        <!-- pivottable, get model from window's composer -->
        <pivottable id="pivottable" model="${win$composer.pivotModel}" />
        <div style="margin-top: 10px;">
            Filter info:
            <label id="lb" />
        </div>
        <!-- filter list -->
        <div id="filter" />
    </window>
</zk>


TestComposer.java

Provide model, listen to onPivotPopup to update filter list, listen to onFilterChanged to update pivottable with filtered data.

package test;

import java.util.List;
import org.zkoss.pivot.Pivottable;

import org.zkoss.pivot.event.PivotUIEvent;
import org.zkoss.pivot.impl.TabularPivotField;
import org.zkoss.pivot.impl.TabularPivotModel;
import org.zkoss.zk.ui.select.SelectorComposer;
import org.zkoss.zk.ui.select.annotation.Listen;
import org.zkoss.zk.ui.select.annotation.Wire;
import org.zkoss.zul.Div;
import org.zkoss.zul.Label;

/**
 * Tested with ZK 6.0.2 EE and ZK Pivottable 2.0.0
 *
 * @author benbai123
 */
@SuppressWarnings("rawtypes")
public class TestComposer extends SelectorComposer {

    private static final long serialVersionUID = -8249566421884806620L;
    @Wire
    Pivottable pivottable;
    @Wire
    Div filter; // div that will contain filter list
    @Wire
    Label lb; // filter info
    // pivot model with the 'whole' raw data
    private TabularPivotModel _pivotModel;
    // model provider, provide the columns, raw data and pivot model
    private PivotModelProvider _modelProvider = new PivotModelProvider();

    // handler to do the works of filter
    // NOTE: Renew it if changed to a complete different model
    private FilterHandler _filterHandler = new FilterHandler();

    /**
     * Get pivottable's model
     * @return TabularPivotModel the pivottable's model
     * @throws Exception
     */
    public TabularPivotModel getPivotModel () throws Exception {
        if (_pivotModel == null) {
            _pivotModel = _modelProvider.getPivotModel();
        }
        return PVTUtils.cloneModelWithData(_pivotModel, getFilteredData());
    }

    /**
     * update the selected field for filter
     * @param e
     */
    @Listen("onPivotPopup = #pivottable")
    public void updateFilterIndex (PivotUIEvent e) {
        if (e.getRowContext() != null
            && e.getColumnContext() == null) {
            // clicked on row field
            updateFilter(_pivotModel.getRowFields()[e.getRowContext().getNode().getDepth()-1]);
        } else if (e.getRowContext() == null
                && e.getColumnContext() != null) {
            // clicked on column field
            updateFilter(_pivotModel.getColumnFields()[e.getColumnContext().getNode().getDepth()-1]);
        }
    }
    /**
     * called while filter is changed
     * update filter value of selected field
     * @param event
     */
    @Listen("onFilterChanged = #pivottable")
    public void updateLimit (FilterChangedEvent event) {
        _filterHandler.updateLimit(event.getFieldName(), event.getValue(), event.isChecked());
        updatePivottable();
    }
    /**
     * update the filter list
     * @param field the field to update filter list
     */
    private void updateFilter (TabularPivotField field) {
        _filterHandler.updateFilter(pivottable,
                field,
                _modelProvider.getColumns(),
                getRawData(),
                filter);
        // update field info label
        lb.setValue("field type: " + field.getType() + ", field name: " + field.getFieldName());
    }
    /**
     * update pivottable with filtered pivot model
     */
    private void updatePivottable () {
        // store current structure at first
        PVTUtils.syncModelStructure((TabularPivotModel)pivottable.getModel(), _pivotModel);
        TabularPivotModel filteredModel = PVTUtils.cloneModelWithData(_pivotModel, getFilteredData());
        pivottable.setModel(filteredModel);
    }
    /**
     * get the filtered data
     * @return
     */
    private Iterable<List<Object>> getFilteredData () {
        return _filterHandler.filterData(_pivotModel, _modelProvider.getColumns(), getRawData());
    }
    /**
     * get complete raw data
     * @return
     */
    @SuppressWarnings("unchecked")
    private Iterable<List<Object>> getRawData () {
        return (Iterable<List<Object>>)_pivotModel.getRawData();
    }
}


PivotModelProvider.java

Provide base model, raw data and columns.

package test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

import org.zkoss.pivot.PivotField;
import org.zkoss.pivot.impl.TabularPivotModel;

public class PivotModelProvider {
    // pivot model with the 'whole' raw data
    private TabularPivotModel _pivotModel;
    /**
     * Get pivottable's model
     * @return TabularPivotModel the pivottable's model
     * @throws Exception
     */
    public TabularPivotModel getPivotModel () {
        if (_pivotModel == null) {
            _pivotModel = new TabularPivotModel(getData(), getColumns());

            // assign rows, the order matches to the level of row node field
            _pivotModel.setFieldType("Row_01", PivotField.Type.ROW);
            _pivotModel.setFieldType("Row_02", PivotField.Type.ROW);
            _pivotModel.setFieldType("Row_03", PivotField.Type.ROW);

            // assign columns, the order matches to the level of column node field
            _pivotModel.setFieldType("Column_01", PivotField.Type.COLUMN);
            _pivotModel.setFieldType("Column_02", PivotField.Type.COLUMN);

            // assign datas, the order matches to the order of data field
            _pivotModel.setFieldType("Data_01", PivotField.Type.DATA);
            _pivotModel.setFieldType("Data_02", PivotField.Type.DATA);
            _pivotModel.setFieldType("Data_03", PivotField.Type.DATA);
        }
        return _pivotModel;
    }
    /**
     * prepare columns name for pivottable's model
     * @return
     */
    public List<String> getColumns() {
        return Arrays.asList(new String[]{
                "Row_01", "Row_02", "Row_03",
                "Column_01", "Column_02",
                "Data_01", "Data_02", "Data_03"
        });
    }
    /**
     * prepare the data for pivottable's model
     * The order of object put into data list matches
     * the order of column name's order
     * @return
     * @throws Exception
     */
    private List<List<Object>> getData() {
        List<List<Object>> result = new ArrayList<List<Object>>();
        Random r = new Random();

        for (int i = 0; i < 100; i++) {
            List<Object> data = new ArrayList<Object>();
            data.add("Row_01 - " + (r.nextInt(5) + 1));
            data.add("Row_02 - " + (r.nextInt(5) + 1));
            data.add("Row_03 - " + (r.nextInt(5) + 1));
            data.add("Column_01 - " + (r.nextInt(5) + 1));
            data.add("Column_02 - " + (r.nextInt(5) + 1));
            data.add(r.nextInt(10000));
            data.add(r.nextDouble() * 10000.0);
            data.add(r.nextInt(100));
            result.add(data);
        }
        return result;
    }
}


FilterHandler.java

Handle filter task

package test;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.zkoss.pivot.Pivottable;
import org.zkoss.pivot.impl.TabularPivotField;
import org.zkoss.pivot.impl.TabularPivotModel;
import org.zkoss.pivot.util.PivotModels;
import org.zkoss.zk.ui.event.CheckEvent;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zul.Checkbox;
import org.zkoss.zul.Div;
import org.zkoss.zul.Listbox;
import org.zkoss.zul.Listcell;
import org.zkoss.zul.Listitem;

/**
 * Class to handle tasks with respect to filter
 *
 * NOTE: A FilterHandler is rely on a specific set of fields and
 * should be renew after the fields of pivot model are changed 
 * 
 * Tested with ZK 6.0.2 EE and ZK Pivottable 2.0.0
 *
 * @author benbai123
 */
public class FilterHandler {
    /**
     * map that contains all Limit object
     * use field name as the key
     * 
     */
    private Map<String, Limit> _fieldsLimitsMap = new HashMap<String, Limit>();
    /**
     * map that contains all field index
     * use field name as the key
     */
    private Map<String, Integer> _indexMap = new HashMap<String, Integer>();;

    @SuppressWarnings("rawtypes")
    public void updateFilter (Pivottable pivottable, TabularPivotField field, List<String> columns, Iterable<List<Object>> rawData, Div filter) {
        List distinctValues = PVTUtils.getDistinctValues(rawData, columns, getFieldIndex(columns, field.getFieldName()));
        updateFilterList(pivottable, distinctValues, field, filter);
    }
    /**
     * update the limited values of a field
     * @param fieldName the name of the field to update
     * @param value the value to update
     * @param accept whether accept the value above
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public void updateLimit (String fieldName, Object value, boolean accept) {
        // try find Limit object
        Limit limit = _fieldsLimitsMap.get(fieldName);
        // create a new one if not found
        if (limit == null) {
            limit = new Limit(fieldName, new HashSet());
            _fieldsLimitsMap.put(fieldName, limit);
        }
        // remove value from limited values if the value is accepted
        // add value to limited values if the value is not accepted
        if (accept) {
            limit.getLimitedValues().remove(value);
        } else {
            limit.getLimitedValues().add(value);
        }
    }
    /**
     * filter data by limits
     * @param model pivot model, get all row/column fields from it
     * @param rawData raw data, to filter it
     * @return the filtered raw data
     */
    public Iterable<List<Object>> filterData (TabularPivotModel model, final List<String> columns, Iterable<List<Object>> rawData) {
        // field name of row/column fields
        final List<String> rcColumns = new ArrayList<String>();
        // keep a final object of limit map so can be used in inner class
        final Map<String, Limit> limits = _fieldsLimitsMap;
        // add all row/column field names
        for (TabularPivotField tpf : model.getRowFields()) {
            rcColumns.add(tpf.getFieldName());
        }
        for (TabularPivotField tpf : model.getColumnFields()) {
            rcColumns.add(tpf.getFieldName());
        }
        return PivotModels.filter(rawData, new PivotModels.Filter<List<Object>>() {
            public boolean keep(List<Object> row) {
                // for each row/column (field names)
                for (String s : rcColumns) {
                    // find Limit object
                    Limit l = limits.get(s);
                    if (l != null) {
                        // find value index
                        int index = getFieldIndex(columns, s);
                        // get value
                        Object value = row.get(index);
                        // do not keep the value if the value is in limit
                        if (l.getLimitedValues().contains(value)) {
                            return false;
                        }
                    }
                }
                return true;
            }
        });
    }
    /**
     * create the filter list
     * @param pivottable pivottable, to fire onFilterChanged event as needed
     * @param distinctValues all different values, used to construct the filter list
     * @param field pivot field, used to get fieldName
     * @param filter the div component specified in index.zul
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    private void updateFilterList (final Pivottable pivottable, List distinctValues, final TabularPivotField field, Div filter) {
        // clear old children
        filter.getChildren().clear();

        Listbox lb = new Listbox();
        lb.setWidth("200px");
        final String fieldName = field.getFieldName();
        Limit limit = _fieldsLimitsMap.get(fieldName);

        // for each value of this field
        for (final Object value : distinctValues) {
            Listitem li = new Listitem();
            Listcell lc = new Listcell();
            Checkbox cb = new Checkbox(value.toString());

            // update checked status of checkbox according to
            // whether this value is a limited value
            if (limit != null && limit.getLimitedValues().contains(value)) {
                cb.setChecked(false);
            } else {
                cb.setChecked(true);
            }
            cb.setParent(lc);
            lc.setParent(li);
            li.setParent(lb);

            // add onCheck event listener to checkbox
            cb.addEventListener("onCheck", new EventListener() {
                public void onEvent (Event event) {
                    // fire event to pivottable with
                    // the information of changed filter attributes
                    Events.postEvent(new FilterChangedEvent(pivottable, fieldName, value, ((CheckEvent)event).isChecked() ));
                }
            });
        }
        // add listbox to div
        lb.setParent(filter);
    }
    /**
     * get the index in raw data of a field
     * @param columns columns used in pivot model
     * @param fieldName name of the field to search index
     * @return int the found index
     */
    private int getFieldIndex (List<String> columns, String fieldName) {
        // search it from index map at first
        if (!_indexMap.containsKey(fieldName)) {
            int index = PVTUtils.getFieldIndex(columns, fieldName);
            // store it to index map
            _indexMap.put(fieldName, index);
        }
        return _indexMap.get(fieldName);
    }
}


PVTUtils.java

Utils of pivottable

package test;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.zkoss.pivot.PivotField;
import org.zkoss.pivot.PivotHeaderNode;
import org.zkoss.pivot.impl.TabularPivotField;
import org.zkoss.pivot.impl.TabularPivotModel;
/**
 * utilities for pivottable
 * 
 * Tested with ZK 6.0.2 EE and ZK Pivottable 2.0.0
 *
 * @author benbai123
 */
public class PVTUtils {
    /**
     * Create a new pivot model based on
     * current pivot model and new data 
     * @param model
     * @param newData
     * @return
     */
    public static TabularPivotModel cloneModelWithData (TabularPivotModel model, Iterable<List<Object>>newData) {
        TabularPivotField[] fields = model.getFields();

        // get columns from old model
        List<String> columns = new ArrayList<String>();
        // set field
        for (TabularPivotField tpf : fields) {
            columns.add(tpf.getFieldName());
        }

        TabularPivotModel newModel = new TabularPivotModel(newData, columns);
        PVTUtils.syncModelStructure(model, newModel);
        return newModel;
    }
    /**
     * get the index in raw data of a field
     * @param columns columns used in pivot model
     * @param fieldName name of the field to search index
     * @return int the found index
     */
    public static int getFieldIndex (List<String> columns, String fieldName) {
        int index = -1;
        // search field name in columns
        for (int i = 0; i < columns.size(); i++) {
            if (columns.get(i).equals(fieldName)) {
                index = i;
                break;
            }
        }
        return index;
    }
    /**
     * get all different values of a field
     * @param rawData the raw data to get different values from
     * @param columns all columns in pivot model
     * @param index the index to get value from list
     * @return
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public static List getDistinctValues (Iterable<List<Object>> rawData, List<String> columns, int index) {
        // set used to hold distinct values
        Set s = new HashSet();
        // result list
        List result = new ArrayList();

        if (index == -1) return result;
        // add all value to set directly
        for (List<Object> data : rawData) {
            s.add(data.get(index));
        }
        // copy to list then sort the list
        for (Object o : s) {
            result.add(o);
        }
        Collections.sort(result);
        return result;
    }
    /**
     * sync the structure of pivot model
     * 
     * @param model the base pivot model
     * @param modelTwo the pivot model to adjust its structure
     */
    public static void syncModelStructure (TabularPivotModel model, TabularPivotModel modelTwo) {
        syncFields(model.getRowFields(), modelTwo);
        syncFields(model.getColumnFields(), modelTwo);
        syncFields(model.getDataFields(), modelTwo);
        syncFields(model.getFields(PivotField.Type.UNUSED), modelTwo);
        syncOpenStatus(model.getRowHeaderTree().getRoot(), modelTwo.getRowHeaderTree().getRoot(), false);
        syncOpenStatus(model.getColumnHeaderTree().getRoot(), modelTwo.getColumnHeaderTree().getRoot(), false);
    }
    /**
     * sync pivot fields of pivot model
     * @param fields the base fields
     * @param model the pivot model to adjust its fields
     */
    private static void syncFields (TabularPivotField[] fields, TabularPivotModel model) {
        for (TabularPivotField f : fields) {
            model.setFieldType(f.getFieldName(), f.getType());

            PivotField field = model.getField(f.getFieldName());
            model.setFieldSubtotals(field, f.getSubtotals());
            model.setFieldKeyComparator(field, f.getComparator());
        }
    }
    /**
     * Synchronize the open status of two pivot header trees
     * 
     * @param root the root of the base pivot header tree (or its sub trees)
     * @param rootTwo the root of the pivot header tree (or its sub trees) to sync
     * @param checkAll whether sync whole tree, <br>
     * true: sync whole tree, put every node of base pivot header tree into open list to sync<br>
     * false: sync only current view, only put the displayed node into open list to sync
     */
    private static void syncOpenStatus (PivotHeaderNode root, PivotHeaderNode rootTwo, boolean checkAll) {
        Map<Object, PivotHeaderNode> originalOpenMap = new HashMap<Object, PivotHeaderNode>();

        // sync displayed node only if not checkAll
        // so do not need to scan whole header tree
        for (PivotHeaderNode node : root.getChildren()) {
            // checkAll: sync each node
            // !checkAll: sync displayed node
            if (checkAll
                || (node.getDepth() == 1 || node.getParent().isOpen())) {
                originalOpenMap.put(node.getKey(), node);
            }
        }
        // for each node in children of rootTwo
        for (PivotHeaderNode newNode : rootTwo.getChildren()) {
            PivotHeaderNode node = originalOpenMap.get(newNode.getKey());
            if (node != null) {
                newNode.setOpen(node.isOpen());
                // recursively sync sub trees
                syncOpenStatus(node, newNode, checkAll);
            }
        }
    }
}


Limit.java

Contains the restricted values of a pivot field

package test;

import java.util.HashSet;
import java.util.Set;

/**
 * Class for hold limited values of a field
 * 
 * Tested with ZK 6.0.2 EE and ZK Pivottable 2.0.0
 *
 * @author benbai123
 *
 */
public class Limit {
    // the name to represent a specific field
    private String _fieldName;
    // the limited values
    private Set<Object> _limitedValues;
    public Limit (String fieldName, Set<Object> limitedValues) {
        _fieldName = fieldName;
        _limitedValues = limitedValues;
        if (_limitedValues == null) {
            _limitedValues = new HashSet<Object>();
        }
    }
    public String getFieldName () {
        return _fieldName;
    }
    public Set<Object> getLimitedValues () {
        return _limitedValues;
    }
}


FilterChangedEvent.java

Event that will be fired when the filter is changed.

package test;

import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.event.Event;

/**
 * Event used to pass the information of updated filter
 * 
 * Event name is "onFilterChanged"
 * 
 * Tested with ZK 6.0.2 EE and ZK Pivottable 2.0.0
 *
 * @author benbai123
 *
 */
public class FilterChangedEvent extends Event {

    private static final long serialVersionUID = 5055917746546499563L;
    /**
     * whether a value is checked in filter list,
     * true: checked (denotes this value is allowed)
     * false: not checked (denotes this value is not allowed)
     */
    private boolean _checked = true;
    /**
     * the updated filter value
     */
    private Object _value;
    /**
     * the field related to the updated filter
     */
    private String _fieldName;
    // constructor, 
    public FilterChangedEvent (Component target, String fieldName, Object value, boolean checked) {
        super("onFilterChanged", target);
        _checked = checked;
        _fieldName = fieldName;
        _value = value;
    }
    public String getFieldName () {
        return _fieldName;
    }
    public Object getValue () {
        return _value;
    }
    public boolean isChecked () {
        return _checked;
    }
}


Reference

Filtering input data
http://books.zkoss.org/wiki/ZK_Pivottable_Essentials/Working_With_Pivottable/Prepare_Data#Filtering_input_data

The Result

View demo on line
http://screencast.com/t/XO77z12jf

Download

Full project at github
https://github.com/benbai123/ZK_Practice/tree/master/Components/projects/Addon_Practice/PivottableTest/FilterRowColumnHeader

Demo Flash
https://github.com/benbai123/ZK_Practice/blob/master/Components/demos/addon/pvt_filter_row_column_header.swf

Friday, April 5, 2013

ZK Macro Component: Using Macro Component to Create a Keypad Component


Introduction

This article describe how to create a keypad component by ZK macro component.

Specification

A keypad, constructs buttons in a popup based on the given charSeq, and update the given input element while button clicked.

charSeq:

[break] denotes make a new line for remaining buttons
[Del] denotes a functional button that will perform delete action
[CapsLock] denotes a functional button works as the CapsLock on common keybord

Note: To display '[' button, you should wrap it by [] (i.e., [[]) since it is a special char in keypad component.

inp:

The input element to work with.

open:

Open/Close the keypad.


The Result

View demo on line
http://screencast.com/t/LrrCTySFv6q


The Program

keypad_test.zul

Test page to test keypad component, define keypad component at the top then using it as a component.

<!-- define macro component -->
<?component name="keypad" macroURI="/folders/macrocomponents/keypad/keypad.zul"
    class="test.marcro.component.keypad.Keypad"?>
<zk>
    <div apply="org.zkoss.bind.BindComposer"
        viewModel="@id('vm') @init('test.marcro.component.keypad.KeypadVM')"
        style="margin: 50px;">
        <hlayout>
            <textbox onFocus="@command('focusInput')" value="@bind(vm.txtValue)" />
            <label value="@load(vm.txtValue)" />
        </hlayout>
        <hlayout>
            <intbox onFocus="@command('focusInput')" value="@bind(vm.intValue)" />
            <label value="@load(vm.intValue)" />
        </hlayout>
        <hlayout>
            <doublebox onFocus="@command('focusInput')" value="@bind(vm.doubleValue)" />
            <label value="@load(vm.doubleValue)" />
        </hlayout>
        <!-- use macro component -->
        <keypad charSeq="@load(vm.charSeq)" inp="@load(vm.inp)"
            open="@bind(vm.open)" />
    </div>
</zk>


KeypadVM.java

VM to test keypad component, contains some setter/getter and process focus event to update status/properties.

package test.marcro.component.keypad;

import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.ContextParam;
import org.zkoss.bind.annotation.ContextType;
import org.zkoss.bind.annotation.NotifyChange;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zul.Doublebox;
import org.zkoss.zul.Intbox;
import org.zkoss.zul.impl.InputElement;

public class KeypadVM {
    // chars 
    private String _charSeq;
    private InputElement _inp;
    private boolean _open;

    private String _txtValue;
    private int _intValue;
    private double _doubleValue;

    public String getCharSeq () {
        return _charSeq;
    }
    public InputElement getInp () {
        return _inp;
    }
    public boolean isOpen () {
        return _open;
    }
    public void setTxtValue (String txtValue) {
        _txtValue = (txtValue == null? "" : txtValue);
    }
    public String getTxtValue () {
        return _txtValue;
    }
    public void setIntValue (Integer intValue) {
        _intValue = (intValue == null? 0 : intValue);
    }
    public Integer getIntValue () {
        return _intValue;
    }
    public void setDoubleValue (Double doubleValue) {
        _doubleValue = (doubleValue == null? 0.0 : doubleValue);
    }
    public Double getDoubleValue () {
        return _doubleValue;
    }
    @Command
    @NotifyChange ({"charSeq", "inp", "open"})
    public void focusInput (@ContextParam(ContextType.TRIGGER_EVENT) Event event) {
        Component comp = event.getTarget();
        if (comp instanceof InputElement) {
            if (comp instanceof Intbox) {
                _charSeq = "123[break]456[break]789[break]0[Del]";
            } else if (comp instanceof Doublebox) {
                _charSeq = "123[break]456[break]789[break].0[Del]";
            } else { // textbox
                // note: wrap '[' by '[' and ']'
                _charSeq = "1234567890-=[break]QWERTYUIOP[[]]\\[break]ASDFGHJKL;'[CapsLock][break]ZXCVBNM,./[Del]";
            }
            _inp = (InputElement)comp;
            _open = true;
        }
    }
}


keypad.zul

This is the zul page for keypad component, define several custom javascript function with a popup component.

<zk xmlns:w="client">
    <script><![CDATA[
        function updateCursorRange (iwgt) {
            var inp = iwgt.getInputNode(), // input element
                range = zk(inp).getSelectionRange(), // get selected range
                start = range[0],
                end = range[1];

            // store start/end in input widget
            iwgt.cursorStartPosition = start;
            iwgt.cursorEndPosition = end;
        }
        function getCursorRange (iwgt) {
            // get the stored start/end
            var start = iwgt.cursorStartPosition, // selected range
                end = iwgt.cursorEndPosition,
                range = [];
            // default 0 if undefined
            range.push(start? start : 0);
            range.push(end? end : 0);
            return range;
        }
    ]]></script>
    <popup id="pp">
        <attribute w:name="open"><![CDATA[
            function (ref, offset, position, opts) {
                // keep the related input widget
                var iwgt = zk.Widget.$(ref);
                this.currentInp = iwgt;
                // override doKeyUp_ and doMouseUp_ to update selected range
                // override fire to reduce ajax call
                this.overrideInp(iwgt);
                // update selected range at first
                // or intbox/doublebox will put char at wrong position
                // since the first mouseup might not the overridden one
                updateCursorRange(iwgt);
                if (this.capsLockEnabled)
                    this.switchCapsLock();
                // call original function
                this.$open(ref, offset, position, opts);
            }
        ]]></attribute>
        <attribute w:name="executeDelete"><![CDATA[
            function () {
                var iwgt = this.currentInp, // input widget
                    range = getCursorRange(iwgt),
                    start = range[0], // selected range, never undefined
                    end = range[1],
                    value = iwgt.getInputNode().value,
                    len = value? value.length : 0;

                // nothing selected
                if (start == end) {
                    if (len >= (end + 1)) {
                        // to delete one char after cursor
                        end++;
                        iwgt.cursorEndPosition = end;
                    } else if (len > 0) {
                        // to delete one char before cursor
                        start--;
                        iwgt.cursorStartPosition = start;
                    }
                }
                // do nothing if no selection
                if (start != end) {
                    // replace selected range by ''
                    // (i.e., delete selected range)
                    this.replaceRange (start, end, '', iwgt);
                }
            }
        ]]></attribute>
        <attribute w:name="executeInsert"><![CDATA[
            function (str) {
                var iwgt = this.currentInp, // input widget
                    range = getCursorRange(iwgt),
                    start = range[0], // selected range, never undefined
                    end = range[1];

                // replace selected range by str
                this.replaceRange (start, end, str, iwgt);
            }
        ]]></attribute>
        <attribute w:name="replaceRange"><![CDATA[
            function (start, end, replacement, iwgt) {
                iwgt.updatingByKeypad = true;
                var inp = iwgt.getInputNode(), // dom input element
                    value = inp.value, // current value
                    len,
                    doubleDotTail;

                // snice doublebox will remov
                // '.' at the tail automatically,
                // plus one '1' and select it automatically
                // to keep the '.' and the selected '1'
                // will be replaced by next input
                if (iwgt.$instanceof(zul.inp.Doublebox)) {
                    if (end == value.length
                        && replacement == '.') {
                        doubleDotTail = true;
                        replacement = '.1';
                    }
                }
                if (!this.capsLockEnabled)
                    replacement = replacement.toLowerCase();
                // replace selected range by replacement
                inp.value = value.substr(0, start) + replacement + value.substr(end);
                // update change if needed,
                iwgt.updateChange_();

                if (replacement
                    && (len = replacement.length)) {
                    var vlen = inp.value.length;
                    if (doubleDotTail) {
                        // to focus the last '1'
                        start = vlen-1;
                        end = vlen;
                    } else {
                        start += len;
                        // in case intbox/doublebox fix 00 to 0
                        if (start > vlen)
                            start = vlen;
                        end = start;
                    }    
                } else {
                    end = start;
                }
                // set cursor back
                zk(inp).setSelectionRange(start, end);
                // update selected range
                updateCursorRange(iwgt);
                iwgt.updatingByKeypad = null;
            }
        ]]></attribute>
        <attribute w:name="bind_"><![CDATA[
            function (a, b, c) {
                this.$bind_(a, b, c);
                // override onFloatUp after bind_
                // since it is not the normal function as bind_
                this.overrideFloatup();
            }
        ]]></attribute>
        <attribute w:name="overrideFloatup"><![CDATA[
            function () {
                if (!this.oldFloatup) {
                    var wgt = this;
                    // keep original function
                    this.oldFloatup = this['onFloatUp'];
                    // override
                    this['onFloatUp'] = function (ctl) {
                        // do nothing if triggered by focus current input widget
                        var cwgt = ctl.origin;
                        if (wgt.currentInp == cwgt)
                            return;
                        wgt.oldFloatup(ctl);
                    }
                }
            }
        ]]></attribute>
        <attribute w:name="switchCapsLock"><![CDATA[
            function () {
                // enable/disable caps lock
                var capsLockEnabled = !this.capsLockEnabled,
                    iwgt = this.currentInp,
                    range = getCursorRange(iwgt),
                    capsLock = jq('.capslock-btn')[0];
                this.capsLockEnabled = capsLockEnabled;
                if (capsLock) {
                    if (capsLockEnabled) {
                        capsLock.style.color = 'green';
                    } else {
                        capsLock.style.color = '';
                    }
                }
                zk(iwgt.getInputNode()).setSelectionRange(range[0], range[1]);
            }
        ]]></attribute>
        <attribute w:name="overrideInp"><![CDATA[
            function (iwgt) {
                // override doKeyUp_
                if (!iwgt.oldKeyUp) {
                    // override doKeyUp_
                    // keep original function
                    iwgt.oldKeyUp = iwgt['doKeyUp_'];
                    // override
                    iwgt['doKeyUp_'] = function (evt) {
                        // run original function
                        iwgt.oldKeyUp(evt);
                        // update selected range
                        updateCursorRange(iwgt);
                    }

                    // override doMouseUp_ similarly
                    iwgt.oldMouseUp = iwgt['doMouseUp_'];
                    iwgt['doMouseUp_'] = function (evt) {
                        iwgt.oldMouseUp(evt);
                        updateCursorRange(iwgt);
                    }

                    // override fire
                    iwgt.oldFire = iwgt['fire'];
                    iwgt['fire'] = function (evtnm, data, opts, timeout) {
                        // delay onChange event if 
                        // updating by keypad to reduce ajax call
                        if ('onChange' == evtnm) {
                            var oldTimer = iwgt.deferEventTimer;
                            if (oldTimer) {
                                clearTimeout(oldTimer);
                                iwgt.deferEventTimer = null;
                            }
                            if (iwgt.updatingByKeypad) {
                                iwgt.deferEventTimer = setTimeout(function () {
                                    iwgt.oldFire(evtnm, data, opts, timeout);
                                    iwgt.deferEventTimer = null;
                                }, 500);
                            } else
                                iwgt.oldFire(evtnm, data, opts, timeout);
                        } else
                            iwgt.oldFire(evtnm, data, opts, timeout);
                    }
                }
            }
        ]]></attribute>
    </popup>
</zk>


Keypad.java

This is the java class for keypad component, handle status of a keypad.

package test.marcro.component.keypad;

import org.zkoss.zk.ui.HtmlMacroComponent;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.OpenEvent;
import org.zkoss.zk.ui.select.annotation.Wire;
import org.zkoss.zul.Button;
import org.zkoss.zul.Hlayout;
import org.zkoss.zul.Popup;
import org.zkoss.zul.impl.InputElement;

/**
 * java class of keypad macro component
 * 
 * @author benbai123
 *
 */
public class Keypad extends HtmlMacroComponent {

    private static final long serialVersionUID = 1436975152609984604L;

    @Wire
    Popup pp;

    /** char sequence, used to generate button in keypad */
    private String _charSeq;
    /** the related input element to update */
    private InputElement _inp;
    /** open status of keypad popup */
    private boolean _open;

    /**
     * Constructor
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public Keypad () {
        compose();
        final Keypad comp = this;
        // since there is no isOpen() in Popup,
        // update open status manually
        pp.addEventListener(Events.ON_OPEN, new EventListener () {
            public void onEvent (Event event) {
                comp.updateOpen(((OpenEvent)event).isOpen());
            }
        });
    }
    // setter/getter
    /**
     * init buttons in keypad
     * 
     * parse format: <br>
     * [break] denotes create a new line (hlayout) for remaining buttons <br>
     * [...] denotes create button by string in [] <br>
     * [Del] and [CapsLock] are two supported function key <br>
     * a single char otherwise<br>
     * 
     * Note: '[' will be considered as start of [...], wrap it by [] (i.e., [[]) as needed
     */
    public void setCharSeq (String charSeq) {
        if (charSeq != null) {
            if (!charSeq.equals(_charSeq)) {
                _charSeq = charSeq;
                initButtons();
            }
        } else {
            _charSeq = null;
        }
    }
    public String getCharSeq () {
        return _charSeq;
    }
    public void setInp (InputElement inp) {
        if (_inp != inp) {
            _inp = inp;
            if (_inp != null && _open) {
                openPopup();
            }
        }
    }
    public InputElement getInp () {
        return _inp;
    }
    public void setOpen (boolean open) {
        if (_open != open) {
            _open = open;
            if (_open
                && _inp != null) {
                openPopup();
            } else {
                pp.close();
            }
        }
    }
    public boolean isOpen () {
        return _open;
    }
    /**
     * open keypad popup
     */
    private void openPopup () {
        pp.open(_inp, "after_center");
    }
    /**
     * init buttons in keypad
     * 
     * parse format: <br>
     * [break] denotes create a new line (hlayout) for remaining buttons <br>
     * [Del] and [CapsLock] are two supported function keys <br>
     * [...] denotes create button by string in [] <br>
     * a single char otherwise<br>
     * 
     * Note: '[' will be considered as start of [...], wrap it by [] (i.e., [[]) as needed
     */
    private void initButtons () {
        // clear old children
        pp.getChildren().clear();
        if (_charSeq != null) {
            int len = _charSeq.length();
            // used to build label of [...] 
            StringBuilder sb = new StringBuilder("");
            // used to contains a line of buttons
            Hlayout hl = new Hlayout();
            // label for create button
            String label = "";
            for (int i = 0; i < len; i++) {
                char ch = _charSeq.charAt(i);
                if (ch != '[') {
                    // single char
                    label = ch + "";
                } else {
                    // find ... in [...]
                    while (i < len) {
                        i++;
                        ch = _charSeq.charAt(i);
                        if (ch == ']') {
                            break;
                        }
                        sb.append(ch);
                    }
                    label = sb.toString();
                    sb.setLength(0);
                }

                if ("break".equals(label)) {
                    // break line
                    hl.setParent(pp);
                    hl = new Hlayout();
                } else {
                    // create button
                    createButton(label).setParent(hl);
                    if (i+1 == len) {
                        hl.setParent(pp);
                    }
                }
            }
        }
    }

    private Button createButton (String label) {
        Button btn = new Button(label);
        String doOriginalClickAndFindParentPopup = "function (evt) {\n" +
                            "        this.$doClick_(evt);\n" + // do original function
                            "        var p = this.parent;\n" + // find the parent popup
                            "        while (p && !p.$instanceof(zul.wgt.Popup))\n" +
                            "            p = p.parent;\n" +
                            "        if (p)\n";
        if ("Del".equals(label)) {
            btn.setWidgetOverride("doClick_",
                    doOriginalClickAndFindParentPopup +
                    "    p.executeDelete()\n" + // call delete function
                    "}");
        } else if ("CapsLock".equals(label)) {
            btn.setSclass("capslock-btn");
            btn.setWidgetOverride("doClick_", 
                    doOriginalClickAndFindParentPopup +
                    "    p.switchCapsLock()\n" + // call switch (enable/disable) caps lock function
                    "}");
        } else {
            if ("\\".equals(label)
                || "'".equals(label)) {
                // special char, need one more escape char at client side
                label = "\\" + label;
            }
            btn.setWidgetOverride("doClick_",
                    doOriginalClickAndFindParentPopup +
                    "    p.executeInsert('"+label+"')\n" + // insert label
                    "}");
        }

        return btn;
    }
    private void updateOpen (boolean open) {
        _open = open;
    }
}


Reference

Related thread at ZK Forum
http://forum.zkoss.org/question/86182/keypad-component/

Macro component document
http://books.zkoss.org/wiki/ZK_Developer's_Reference/UI_Composing/Macro_Component

widget.js
https://github.com/zkoss/zk/blob/master/zk/src/archive/web/js/zk/widget.js

dom.js
https://github.com/zkoss/zk/blob/master/zk/src/archive/web/js/zk/dom.js


Download

keypad_test.zul
https://github.com/benbai123/ZK_Practice/blob/master/Components/projects/Components_Practice/WebContent/folders/macrocomponents/keypad/keypad_test.zul

KeypadVM.java
https://github.com/benbai123/ZK_Practice/blob/master/Components/projects/Components_Practice/src/test/marcro/component/keypad/KeypadVM.java

keypad.zul
https://github.com/benbai123/ZK_Practice/blob/master/Components/projects/Components_Practice/WebContent/folders/macrocomponents/keypad/keypad.zul

Keypad.java
https://github.com/benbai123/ZK_Practice/blob/master/Components/projects/Components_Practice/src/test/marcro/component/keypad/Keypad.java

Demo Flash
https://github.com/benbai123/ZK_Practice/blob/master/Components/demos/macro_component/zk_macro_keypad.swf