Monday, February 25, 2013

ZK Drag and Drop: Make Everything Resizable


Introduction

In ZK, some comopnents like textbox, button, link are not resizable, this article describe how to use a draggable div to make those non resizable components resizable.

The Program

make_components_resizable.zul

A resizer div and several custom resizable components in this page, will display the resizer div while mouseover the right-side or the bottom of custom resizable component, fire event while resizer div is dragged to resize component.

<!-- 
    Tested with ZK 6.0.2 (Chrome, IE9)
 -->
<zk xmlns:w="client">
    <style>
        .custom-sizer {
            position: absolute;
            display: none;
            border: 4px solid #aaa;
            z-index: 999999;
        }
        .custom-resizable-comp {
            display: block;
        }
    </style>
    <script><![CDATA[
        // get the dom element of custom resizer div
        function _getCustomResizer () {
            return jq('.custom-sizer')[0];
        }
        // hide the custom resizer div
        function _hideCustomResizer () {
            if (!zk.Widget.customResizing)
                _getCustomResizer().style.display = 'none';
        }
        // override zk.Widget to make components resizable
        zk.afterLoad("zul", function () {
            var _wgt = {};
            zk.override(zk.Widget.prototype, _wgt, {
                doMouseMove_: function (evt) {
                    _wgt.doMouseMove_.apply(this, arguments);
                    // do nothing if resizing
                    if (zk.Widget.customResizing) return;
                    // is custom resizable component
                    if (this._isResizable()) {
                        // keep widget instance
                        zk.Widget.customResizableWidget = this;
                        // at the right side of the component
                        if (this._atRightSide(evt)) {
                            // keep 'resizing width'
                            zk.Widget.customResizableWidgetDir = 'w';
                            // show custom resizer as vertical bar
                            // at the right side of this component
                            this._showRightResizer();
                        } else if (this._atBottom(evt)) { // at bottom of the component
                            // keep 'resizing height'
                            zk.Widget.customResizableWidgetDir = 'h';
                            // show custom resizer as horizontal bar
                            // at the bottom of this component
                            this._showBottomResizer();
                        } else { // not close to right side/bottom
                            // hide custom resizer
                            _hideCustomResizer();
                        }
                    }
                },
                // is resizable if has the specific class
                // 'custom-resizable-comp'
                _isResizable: function () {
                    return jq(this.$n()).hasClass('custom-resizable-comp');
                },
                // the mouse cursor is near the right side of component
                _atRightSide: function (evt) {
                    var left = evt.pageX, // cursor x position
                        $n = jq(this.$n()),
                        wRight = $n.offset().left + $n.width(); // the right side of component
                    // their distance is smaller than 3px
                    return (Math.abs(left - wRight) < 3);
                },
                // the mouse cursor is near the bottom of component
                _atBottom: function (evt) {
                    var top = evt.pageY, // cursor y position
                        $n = jq(this.$n()),
                        wBottom = $n.offset().top + $n.height(); // the bottom of component
                    // their distance is smaller than 3px
                    return (Math.abs(top - wBottom) < 3);
                },
                // display custom resizer at the right side of component
                _showRightResizer: function () {
                    // do nothing if resizing
                    if (zk.Widget.customResizing) return;
                    var resizer = _getCustomResizer(),
                        rstyle = resizer.style,
                        $n = jq(this.$n()),
                        noffset = $n.offset();

                    // adjust the position of custom resizer
                    rstyle.left = noffset.left + $n.width() + 'px';
                    rstyle.top = noffset.top + 'px';
                    // adjust the size of custom resizer
                    rstyle.width = '0px';
                    rstyle.height = $n.height() + 'px';
                    // adjust cursor and show custom resizer
                    rstyle.cursor = 'e-resize';
                    rstyle.display = 'block';
                },
                _showBottomResizer: function () {
                    if (zk.Widget.customResizing) return;
                    var resizer = _getCustomResizer(),
                        rstyle = resizer.style,
                        $n = jq(this.$n()),
                        noffset = $n.offset();
    
                    // adjust the position of custom resizer
                    rstyle.left = noffset.left + 'px';
                    rstyle.top = noffset.top + $n.height() + 'px';
                    // adjust the size of custom resizer
                    rstyle.width = $n.width() + 'px';
                    rstyle.height = '0px';
                    // adjust cursor and show custom resizer
                    rstyle.cursor = 's-resize';
                    rstyle.display = 'block';
                }
            });
        });
    ]]></script>
    <div id="customSizer" sclass="custom-sizer" draggable="true" use="test.custom.component.div.CustomResizer">
        <attribute w:name="doMouseOut_"><![CDATA[
            function (evt) {
                // hide custom resizer if mouseout it
                this.$doMouseOut_(evt);
                _hideCustomResizer();
            }
        ]]></attribute>
        <attribute w:name="getDragOptions_"><![CDATA[
            function (map) {
                var dragOptions = {};
                for (var key in map) {
                    dragOptions[key] = map[key];
                }
                var oldBeforeSizing = dragOptions.starteffect,
                    oldAfterSizing = dragOptions.endeffect,
                    oldDragging = dragOptions.change;
                // at the beginning of dragging
                dragOptions.starteffect = function (dg) {
                    // mark resizing
                    zk.Widget.customResizing = true;
                    // save the starting x position if resizing width
                    // or save the starting y position if resizing height
                    dg._delta = zk.Widget.customResizableWidgetDir == 'w'?
                            dg._currentDelta()[0] : dg._currentDelta()[1];
                    // do original function
                    oldBeforeSizing(dg);
                };
                // at the end of dragging
                dragOptions.endeffect = function (dg, evt) {
                    // remove resizing mark
                    zk.Widget.customResizing = false;
                    // get resizing direction (w: width, h: height)
                    // evaluate new size
                    // get the uuid of the component to resize
                    var resizeWidth = zk.Widget.customResizableWidgetDir == 'w',
                        $szwgt = jq(zk.Widget.customResizableWidget),
                        delta = resizeWidth?
                            (dg._currentDelta()[0] - dg._delta) : (dg._currentDelta()[1] - dg._delta),
                        newValue = resizeWidth?
                            ($szwgt.width() + delta) : ($szwgt.height() + delta),
                        uuid = zk.Widget.customResizableWidget.uuid,
                        data;
                    if (newValue <= 0) newValue = 1;
                    // fire onCustomResize event to custom resizer
                    // 'value', 'reference' and 'resizeAttribute' are
                    // the keys of data map
                    data = {value: newValue,
                            reference: uuid,
                            resizeAttribute: (resizeWidth? 'width' : 'height')};
                    zk.Widget.$('$customSizer').fire('onCustomResize', data);
                    // call original function
                    oldAfterSizing(dg, evt);
                    // hide custom resizer
                    _getCustomResizer().style.display = 'none';
                };
                // while dragging
                dragOptions.change = function (drag, pt, evt) {
                    // call original function
                    oldDragging(drag, pt, evt);
                    var wgt = drag.control,
                        n = wgt.$n(),
                        $szwgt = jq(zk.Widget.customResizableWidget);
                    if (n.style.display != 'block') n.style.display = 'block';
                    // keep 'top' unchanged and make 'left' follow mouse position
                    // if resizing width,
                    // keep 'left' unchanged and make 'top' follow mouse position
                    // if resizing height,
                    n.style.left = (zk.Widget.customResizableWidgetDir == 'h'?
                            $szwgt.offset().left+'px' : evt.pageX+'px');
                    n.style.top = (zk.Widget.customResizableWidgetDir == 'w'?
                            $szwgt.offset().top+'px' : evt.pageY+'px');
                }
                // clear ghosting function to
                // avoid the faker dom element while dragging
                dragOptions.ghosting = null;
                return dragOptions;
            }
        ]]></attribute>
    </div>
    <vlayout>
        <hlayout>
            <textbox sclass="custom-resizable-comp"
                value="resizable textbox" />
            <checkbox sclass="custom-resizable-comp"
                label="resizable checkbox" />
            <vlayout>
                <a sclass="custom-resizable-comp"
                    href="http://www.zkoss.org">
                    resizable link
                </a>
                resizable menubar
                <menubar sclass="custom-resizable-comp">
                    <menu sclass="custom-resizable-comp" label="File (resizable)">
                        <menupopup sclass="custom-resizable-comp">
                            <menuitem sclass="custom-resizable-comp" label="New (resizable)" onClick="alert(self.label)"/>
                            <menuitem sclass="custom-resizable-comp" label="Exit (resizable)" onClick="alert(self.label)"/>
                        </menupopup>
                    </menu>
                </menubar>
            </vlayout>
        </hlayout>
        <button sclass="custom-resizable-comp"
            mold="trendy"
            label="resizable button" />
    </vlayout>
</zk>


CustomResizer.java

The class extends div, listening to custom resize event and resizing component as needed.

package test.custom.component.div;

import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.HtmlBasedComponent;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zul.Div;
import org.zkoss.zul.impl.XulElement;

import test.custom.component.event.CustomResizeEvent;

/**
 * Tested with ZK 6.0.2
 * @author benbai123
 *
 */
public class CustomResizer extends Div {
    private static final long serialVersionUID = 5597493971151879186L;

    static {
        // listen to custom resize event
        addClientEvent(CustomResizer.class, CustomResizeEvent.ON_CUSTOM_RESIZE, CE_DUPLICATE_IGNORE
                | CE_IMPORTANT | CE_NON_DEFERRABLE);
    }
    public void service(org.zkoss.zk.au.AuRequest request, boolean everError) {
        final String cmd = request.getCommand();

        // custom resize event
        if (CustomResizeEvent.ON_CUSTOM_RESIZE.equals(cmd)) {
            CustomResizeEvent evt = CustomResizeEvent.getCustomResizeEvent(request);
            // get the component to resize
            Component ref = evt.getReference();
            if (ref != null) {
                // get new size
                int value = evt.getValue();
                // get direction (width/height)
                String dir = evt.getResizeAttribute();
                // do resize
                if ("width".equals(dir)) {
                    if (ref instanceof XulElement) {
                        ((XulElement)ref).setWidth(value+"px");
                    } else if (ref instanceof HtmlBasedComponent) {
                        ((HtmlBasedComponent)ref).setWidth(value+"px");
                    } 
                } else if ("height".equals(dir)) {
                    if (ref instanceof XulElement) {
                        ((XulElement)ref).setHeight(value+"px");
                    } else if (ref instanceof HtmlBasedComponent) {
                        ((HtmlBasedComponent)ref).setHeight(value+"px");
                    } 
                }
            }
            // post event
            Events.postEvent(evt);
        } else {
            super.service(request, everError);
        }
    }
}


CustomResizeEvent.java

Used to keep the data of custom resize event.

package test.custom.component.event;

import java.util.Map;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.event.Event;

/**
 * Tested with ZK 6.0.2
 * Basically you can think an Event object
 * is just a POJO to keep the data within an event
 * @author benbai123
 *
 */
public class CustomResizeEvent extends Event {
    private static final long serialVersionUID = -8839289786864776054L;

    public static final String ON_CUSTOM_RESIZE = "onCustomResize";
    private int _value;
    private final Component _reference;
    private String _resizeAttribute;
    @SuppressWarnings("rawtypes")
    public static CustomResizeEvent getCustomResizeEvent (org.zkoss.zk.au.AuRequest request) {
        // get data map
        Map data = request.getData();
        // get values by keys
        int value = (Integer)data.get("value");
        final Component reference = request.getDesktop().getComponentByUuidIfAny((String)data.get("reference"));
        String resizeAttribute = (String)data.get("resizeAttribute");
        // create event instance, return it
        return new CustomResizeEvent(ON_CUSTOM_RESIZE, request.getComponent(), value, reference, resizeAttribute);
    }
    // Constructor
    public CustomResizeEvent (String name, Component target,
            int value, Component reference, String resizeAttribute) {
        super(name, target);
        _value = value;
        _reference = reference;
        _resizeAttribute = resizeAttribute;
    }
    // getters
    public int getValue () {
        return _value;
    }
    public Component getReference () {
        return _reference;
    }
    public String getResizeAttribute () {
        return _resizeAttribute;
    }
}


The Result

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

Reference

Source code of widget.js
https://github.com/zkoss/zk/blob/6.0/zk/src/archive/web/js/zk/widget.js

Source code of drag.js
https://github.com/zkoss/zk/blob/6.0/zk/src/archive/web/js/zk/drag.js

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

Download

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

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

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

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

15 comments:

  1. Do you know Have any idea about this?
    http://stackoverflow.com/questions/15201515/combobox-issue-in-zk-framework
    Is this a ZK issue or my code issue?

    ReplyDelete
    Replies
    1. I've viewed it several days ago but the sample at zkfiddle cannot be scrolled (might related to resolution).

      This seems a known issue but not fixed yet since some performance concern.

      Delete
  2. Hi Ben i am getting a strange issue with abosulteLayout can you guide me what i am doing wrong

    http://stackoverflow.com/questions/15267692/zk-absolute-layout-issue-with-zul-page

    ReplyDelete
    Replies
    1. seems caused by the tr and td of table are missing

      Delete
    2. Hi ben i am using AbsoulteLayout for displaying components in web page my question is that how can i enable or disable drag and drop of components inside AbsoluteChildren . I will want on certain menuclick drag-drop should enable otherwise it work without drag and drop?

      Delete
    3. Yes, you can change the draggable/droppable attribute as needed, official document:

      http://books.zkoss.org/wiki/ZK_Developer's_Reference/UI_Patterns/Drag_and_Drop

      Delete
    4. I have bind these attributes with a Variable and it worked

      Delete
  3. Can we add drop placeholder with absolutelayout right now if we are dropping any component it drop over a component.

    ReplyDelete
    Replies
    1. I'm afraid I don't know what the "drop placeholder" is, is there any sample I can refer to?

      Delete
  4. Yes you can check this http://jqueryui.com/sortable/#placeholder

    ReplyDelete
    Replies
    1. Do you mean if you dragging an absolutechildren then mouseover another absolutechildren, change the position of mouseovered absolutechildren?

      Delete
    2. May be or you can say if we are dragging any absolutechildren over anyother absolutechildren it will work as placeholder right now it drag over another component.

      Delete
    3. It is hard to do, but you can try to specify draggable/droppable attributes properly, please refer to the sample at zkfiddle

      http://zkfiddle.org/sample/1cud977/1-Draggable-droppable-test

      Delete
  5. Can we save drag n drop component state so that we can use same state in future

    ReplyDelete
    Replies
    1. Sure, if you have proper data structure and table schema for it, then you can:

      1. Load data from database.
      2. Modify the layout, order, size, etc of components with data.
      3. Update data with drag-n-drop event, and save it to database as needed.

      Delete