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

4 comments:

  1. Gr8 Work Ben...Will it be added in ZK Official website

    ReplyDelete
    Replies
    1. Do you mean added as a ZK comopnent? If so, I'm not sure, just my personal work in these two days.

      Delete
  2. Hi Ben Bai,

    I am having some issue to refresh the status after updating the details http://forum.zkoss.org/question/101837/zk-how-to-notify-macro-component-dynamically/

    ReplyDelete
    Replies
    1. As the comment said, need to know your current implementation to go further, you can also refer to the official document/blog:

      http://blog.zkoss.org/2013/05/09/manipulate-components-inside-a-macro-component/ ,

      https://www.zkoss.org/wiki/ZK_Developer's_Reference/UI_Composing/Macro_Component

      Delete