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
Gr8 Work Ben...Will it be added in ZK Official website
ReplyDeleteDo you mean added as a ZK comopnent? If so, I'm not sure, just my personal work in these two days.
DeleteHi Ben Bai,
ReplyDeleteI 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/
As the comment said, need to know your current implementation to go further, you can also refer to the official document/blog:
Deletehttp://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