Click here to Skip to main content
15,891,372 members
Articles / Programming Languages / Javascript

Simple HTML5 SVG Move and Resize Tool

Rate me:
Please Sign up or sign in to vote.
5.00/5 (11 votes)
19 Jun 2013CPOL7 min read 75.1K   4.1K   25  
In this article, I explain, step by step, how we can implement a simple tool for moving and resizing HTML elements, using HTML5 SVG.
// MoveAndResizeTool.js - Implementaion for MoveAndResizeTool. 
// This code uses JQuery so, include the JQuery library before including this file.

function WrapWithMoveAndResizeTool(jquerySelector) {
    var resizeTool = new MoveAndResizeTool(jquerySelector);
    resizeTool.show();

    return resizeTool;
}

// Define MoveAndResizeTool constructor
function MoveAndResizeTool(jquerySelector) {
    this.wrappedElements = new Array();
    this.isShown = false;

    var selectedElements = $(jquerySelector);

    for (var elementInx = 0; elementInx < selectedElements.length; elementInx++) {
        var currElement = selectedElements[elementInx];
        this.wrappedElements[elementInx] = new MoveAndResizeElementWrapper(currElement);
    }
}

// Define MoveAndResizeTool prototype
MoveAndResizeTool.prototype.show = function () {
    if (this.isShown == false) {
        for (var elementInx = 0; elementInx < this.wrappedElements.length; elementInx++) {
            var currElement = this.wrappedElements[elementInx];
            currElement.showWrapper();
        }

        this.isShown = true;
    }
}

MoveAndResizeTool.prototype.hide = function () {
    if (this.isShown == true) {
        for (var elementInx = 0; elementInx < this.wrappedElements.length; elementInx++) {
            var currElement = this.wrappedElements[elementInx];
            currElement.hideWrapper();
        }

        this.isShown = false;
    }
}

var MoveAndResizeTool_ElementWrapper_wrappersCounter = 0;

// Define MoveAndResizeElementWrapper constructor
function MoveAndResizeElementWrapper(elementToWrap) {
    this.originalElement = elementToWrap;

    // Since we want a unique id for each wrapper, we add a counter value to the end of each id.
    MoveAndResizeTool_ElementWrapper_wrappersCounter++;

    this.wrapperId = 'MoveAndResizeTool_ElementWrapper' +
        MoveAndResizeTool_ElementWrapper_wrappersCounter.toString();

    this.wrapperStr = '<div style="position:relative" id="' + this.wrapperId + '">' +
        '<div style="left:8px;top:8px;position:absolute" class="internalWrapper"></div>' +
        '</div>';

    this.externalWrapperQueryStr = '#' + this.wrapperId;
    this.internalWrapperQueryStr = this.externalWrapperQueryStr + ' .internalWrapper';

    // Query strings for the action-triggers.
    this.moveActionTriggerQueryStr = this.externalWrapperQueryStr + ' .moveActionTrigger';
    this.topActionTriggerQueryStr = this.externalWrapperQueryStr + ' .topActionTrigger';
    this.bottomActionTriggerQueryStr = this.externalWrapperQueryStr + ' .bottomActionTrigger';
    this.leftActionTriggerQueryStr = this.externalWrapperQueryStr + ' .leftActionTrigger';
    this.rightActionTriggerQueryStr = this.externalWrapperQueryStr + ' .rightActionTrigger';
    this.topLeftActionTriggerQueryStr = this.externalWrapperQueryStr + ' .topLeftActionTrigger';
    this.topRightActionTriggerQueryStr = this.externalWrapperQueryStr + ' .topRightActionTrigger';
    this.bottomLeftActionTriggerQueryStr = this.externalWrapperQueryStr + ' .bottomLeftActionTrigger';
    this.bottomRightActionTriggerQueryStr = this.externalWrapperQueryStr + ' .bottomRightActionTrigger';

    // Query strings for the resizing border's drawings.
    this.topDrawingQueryStr = this.externalWrapperQueryStr + ' .topDrawing';
    this.bottomDrawingQueryStr = this.externalWrapperQueryStr + ' .bottomDrawing';
    this.leftDrawingQueryStr = this.externalWrapperQueryStr + ' .leftDrawing';
    this.rightDrawingQueryStr = this.externalWrapperQueryStr + ' .rightDrawing';
    this.topLeftDrawingQueryStr = this.externalWrapperQueryStr + ' .topLeftDrawing';
    this.topRightDrawingQueryStr = this.externalWrapperQueryStr + ' .topRightDrawing';
    this.bottomLeftDrawingQueryStr = this.externalWrapperQueryStr + ' .bottomLeftDrawing';
    this.bottomRightDrawingQueryStr = this.externalWrapperQueryStr + ' .bottomRightDrawing';

    this.currentAction = this.ActionsEnum.None;

    this.lastMouseX = 0;
    this.lastMouseY = 0;
}

// Define MoveAndResizeElementWrapper prototype
MoveAndResizeElementWrapper.prototype.ActionsEnum = {
    None: 0,
    LeftResize: 1,
    TopResize: 2,
    RightResize: 3,
    BottomResize: 4,
    TopLeftResize: 5,
    BottomLeftResize: 6,
    TopRightResize: 7,
    BottomRightResize: 8,
    Move: 9
}

MoveAndResizeElementWrapper.prototype.cornerActionTriggerRadius = 8;

MoveAndResizeElementWrapper.prototype.resizingBorderStr =
    '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" style="left:0px;top:0px;position:relative;width:100%;height:100%" >' +
    '<style type="text/css"> .actionTrigger { transition: opacity 0.5s; opacity: 0;} .actionTrigger:hover{transition: opacity 0.3s;opacity: 0.3;}</style>' +
    '<line x1="0" y1="0" x2="100%" y2="0" stroke="#808080" stroke-width="1" stroke-dasharray="5,5" class="topDrawing" />' +
    '<line x1="0" y1="100%" x2="100%" y2="100%" stroke="#808080" stroke-width="1" stroke-dasharray="5,5" class="bottomDrawing" />' +
    '<line x1="0" y1="0" x2="0" y2="100%" stroke="#808080" stroke-width="1" stroke-dasharray="5,5" class="leftDrawing" />' +
    '<line x1="100%" y1="0" x2="100%" y2="100%" stroke="#808080" stroke-width="1" stroke-dasharray="5,5" class="rightDrawing" />' +
    '<circle cx="0" cy="0" r="3" stroke="#0000FF" stroke-width="1" fill="#CCCCFF" class="topLeftDrawing" />' +
    '<circle cx="100%" cy="0" r="3" stroke="#0000FF" stroke-width="1" fill="#CCCCFF" class="topRightDrawing" />' +
    '<circle cx="0" cy="100%" r="3" stroke="#0000FF" stroke-width="1" fill="#CCCCFF" class="bottomLeftDrawing" />' +
    '<circle cx="100%" cy="100%" r="3" stroke="#0000FF" stroke-width="1" fill="#CCCCFF" class="bottomRightDrawing" />' +
    '<rect x="0" y="0" width="100%" height="100%" fill-opacity="0.5" opacity="0" class="actionTrigger moveActionTrigger" style="cursor:move" />' +
    '<line x1="0" y1="0" x2="100%" y2="0" stroke="#000" stroke-width="5" opacity="0" class="actionTrigger topActionTrigger" style="cursor:n-resize" />' +
    '<line x1="0" y1="100%" x2="100%" y2="100%" stroke="#000" stroke-width="5" opacity="0" class="actionTrigger bottomActionTrigger" style="cursor:s-resize" />' +
    '<line x1="0" y1="0" x2="0" y2="100%" stroke="#000" stroke-width="5" opacity="0" class="actionTrigger leftActionTrigger" style="cursor:w-resize" />' +
    '<line x1="100%" y1="0" x2="100%" y2="100%" stroke="#000" stroke-width="5" opacity="0" class="actionTrigger rightActionTrigger" style="cursor:e-resize"/>' +
    '<circle cx="0" cy="0" r="8" stroke="#000" stroke-width="0" fill="#000" opacity="0" class="actionTrigger topLeftActionTrigger" style="cursor:nw-resize" />' +
    '<circle cx="100%" cy="0" r="8" stroke="#000" stroke-width="0" fill="#000" opacity="0" class="actionTrigger topRightActionTrigger" style="cursor:ne-resize" />' +
    '<circle cx="0" cy="100%" r="8" stroke="#000" stroke-width="0" fill="#000" opacity="0" class="actionTrigger bottomLeftActionTrigger" style="cursor:sw-resize" />' +
    '<circle cx="100%" cy="100%" r="8" stroke="#000" stroke-width="0" fill="#000" opacity="0" class="actionTrigger bottomRightActionTrigger" style="cursor:se-resize" />' +
    '</svg>';

MoveAndResizeElementWrapper.prototype.showWrapper = function () {
    this.addWrapperElements();
    this.initializeEventHandlers();
}

MoveAndResizeElementWrapper.prototype.hideWrapper = function () {
    // Set original element's position, to be in the same position, after the wrapper is removed.
    var wrapperLeft = parseInt($(this.externalWrapperQueryStr).css('left'));
    var wrapperTop = parseInt($(this.externalWrapperQueryStr).css('top'));
    var elemLeft = (wrapperLeft + this.cornerActionTriggerRadius) + 'px';
    var elemTop = (wrapperTop + this.cornerActionTriggerRadius) + 'px';
    $(this.originalElement).css('left', elemLeft);
    $(this.originalElement).css('top', elemTop);
    $(this.originalElement).css('position', $(this.externalWrapperQueryStr).css('position'));

    // Put the original element instead of the wrapped element.
    $(this.externalWrapperQueryStr).replaceWith(this.originalElement);
}

MoveAndResizeElementWrapper.prototype.addWrapperElements = function () {
    // Wrap the original element with a resizing border.
    $(this.originalElement).wrap(this.wrapperStr);
    $(this.internalWrapperQueryStr).after(this.resizingBorderStr);

    // Set the external wrapper's position to be 8 (the radius of the corner action trigger) pixels less than the original element's position.
    var elemLeft = parseInt($(this.originalElement).css('left'));
    var elemTop = parseInt($(this.originalElement).css('top'));
    var wrapperLeft = (elemLeft - this.cornerActionTriggerRadius) + 'px';
    var wrapperTop = (elemTop - this.cornerActionTriggerRadius) + 'px';
    $(this.externalWrapperQueryStr).css('left', wrapperLeft);
    $(this.externalWrapperQueryStr).css('top', wrapperTop);
    $(this.externalWrapperQueryStr).css('position', $(this.originalElement).css('position'));

    // Set original element's position to be at the top-left corner of the internal wrapper.
    $(this.originalElement).css('left', 0);
    $(this.originalElement).css('top', 0);
    $(this.originalElement).css('position', 'relative');

    this.adjustWrapper();
}

MoveAndResizeElementWrapper.prototype.initializeEventHandlers = function () {
    var wrapper = this;

    $(this.moveActionTriggerQueryStr).mousedown(function (event) {
        wrapper.currentAction = wrapper.ActionsEnum.Move;
    });

    $(this.topActionTriggerQueryStr).mousedown(function (event) {
        wrapper.currentAction = wrapper.ActionsEnum.TopResize;
    });

    $(this.bottomActionTriggerQueryStr).mousedown(function (event) {
        wrapper.currentAction = wrapper.ActionsEnum.BottomResize;
    });

    $(this.leftActionTriggerQueryStr).mousedown(function (event) {
        wrapper.currentAction = wrapper.ActionsEnum.LeftResize;
    });

    $(this.rightActionTriggerQueryStr).mousedown(function (event) {
        wrapper.currentAction = wrapper.ActionsEnum.RightResize;
    });

    $(this.topLeftActionTriggerQueryStr).mousedown(function (event) {
        wrapper.currentAction = wrapper.ActionsEnum.TopLeftResize;
    });

    $(this.topRightActionTriggerQueryStr).mousedown(function (event) {
        wrapper.currentAction = wrapper.ActionsEnum.TopRightResize;
    });

    $(this.bottomLeftActionTriggerQueryStr).mousedown(function (event) {
        wrapper.currentAction = wrapper.ActionsEnum.BottomLeftResize;
    });

    $(this.bottomRightActionTriggerQueryStr).mousedown(function (event) {
        wrapper.currentAction = wrapper.ActionsEnum.BottomRightResize;
    });

    $(document).mouseup(function (event) {
        // Clear the current action.
        wrapper.currentAction = wrapper.ActionsEnum.None;
    });

    $(document).mousemove(function (event) {
        wrapper.onMouseMove(event);
    });
}

MoveAndResizeElementWrapper.prototype.onMouseMove = function (event) {
    var currMouseX = event.clientX;
    var currMouseY = event.clientY;

    var deltaX = currMouseX - this.lastMouseX;
    var deltaY = currMouseY - this.lastMouseY;

    this.applyMouseMoveAction(deltaX, deltaY);

    this.lastMouseX = event.pageX;
    this.lastMouseY = event.pageY;
}

MoveAndResizeElementWrapper.prototype.applyMouseMoveAction = function (deltaX, deltaY) {
    var deltaTop = 0;
    var deltaLeft = 0;
    var deltaWidth = 0;
    var deltaHeight = 0;

    if (this.currentAction == this.ActionsEnum.RightResize ||
             this.currentAction == this.ActionsEnum.TopRightResize ||
             this.currentAction == this.ActionsEnum.BottomRightResize) {
        deltaWidth = deltaX;
    }

    if (this.currentAction == this.ActionsEnum.LeftResize ||
             this.currentAction == this.ActionsEnum.TopLeftResize ||
             this.currentAction == this.ActionsEnum.BottomLeftResize) {
        deltaWidth = -deltaX;
        deltaLeft = deltaX;
    }

    if (this.currentAction == this.ActionsEnum.BottomResize ||
             this.currentAction == this.ActionsEnum.BottomLeftResize ||
             this.currentAction == this.ActionsEnum.BottomRightResize) {
        deltaHeight = deltaY;
    }

    if (this.currentAction == this.ActionsEnum.TopResize ||
             this.currentAction == this.ActionsEnum.TopLeftResize ||
             this.currentAction == this.ActionsEnum.TopRightResize) {
        deltaHeight = -deltaY;
        deltaTop = deltaY;
    }

    if (this.currentAction == this.ActionsEnum.Move) {
        deltaLeft = deltaX;
        deltaTop = deltaY;
    }

    this.updatePosition(deltaLeft, deltaTop);
    this.updateSize(deltaWidth, deltaHeight);    
    this.adjustWrapper();
}

MoveAndResizeElementWrapper.prototype.updateSize = function (deltaWidth, deltaHeight) {
    // Calculate the new size.
    var elemWidth = parseInt($(this.originalElement).width());
    var elemHeight = parseInt($(this.originalElement).height());
    var newWidth = elemWidth + deltaWidth;
    var newHeight = elemHeight + deltaHeight;

    // Don't allow a too small size.
    var minumalSize = this.cornerActionTriggerRadius * 2;
    if (newWidth < minumalSize) {
        newWidth = minumalSize;
    }
    if (newHeight < minumalSize) {
        newHeight = minumalSize;
    }

    // Set the new size.
    $(this.originalElement).css('width', newWidth + 'px');
    $(this.originalElement).css('height', newHeight + 'px');
}

MoveAndResizeElementWrapper.prototype.updatePosition = function (deltaLeft, deltaTop) {
    // Calculate the new position.
    var elemLeft = parseInt($(this.externalWrapperQueryStr).css('left'));
    var elemTop = parseInt($(this.externalWrapperQueryStr).css('top'));
    var newLeft = elemLeft + deltaLeft;
    var newTop = elemTop + deltaTop;

    // Set the new position.
    $(this.externalWrapperQueryStr).css('left', newLeft + 'px');
    $(this.externalWrapperQueryStr).css('top', newTop + 'px');
}

MoveAndResizeElementWrapper.prototype.adjustWrapper = function () {
    var elemWidth = parseInt($(this.originalElement).width());
    var elemHeight = parseInt($(this.originalElement).height());
    var externalWrapperWidth = (elemWidth + this.cornerActionTriggerRadius * 2) + 'px';
    var externalWrapperHeight = (elemHeight + this.cornerActionTriggerRadius * 2) + 'px';

    $(this.internalWrapperQueryStr).width($(this.originalElement).width());
    $(this.internalWrapperQueryStr).height($(this.originalElement).height());
    $(this.externalWrapperQueryStr).width(externalWrapperWidth);
    $(this.externalWrapperQueryStr).height(externalWrapperHeight);

    // Adjust the resizing border.
    this.adjustResizingBorder();
}

MoveAndResizeElementWrapper.prototype.adjustResizingBorder = function () {
    var elemWidth = parseInt($(this.originalElement).width());
    var elemHeight = parseInt($(this.originalElement).height());

    // Get the minimum and maximum values for X and Y.
    var minX = this.cornerActionTriggerRadius + 'px';
    var minY = this.cornerActionTriggerRadius + 'px';
    var maxX = (this.cornerActionTriggerRadius + elemWidth) + 'px';
    var maxY = (this.cornerActionTriggerRadius + elemHeight) + 'px';

    // Adjust moving rectange.
    this.setRectangleAttributes(this.moveActionTriggerQueryStr, minX, minY, elemWidth + 'px', elemHeight + 'px');

    // Adjust resizing border lines.
    this.setLineAttributes(this.topDrawingQueryStr, minX, minY, maxX, minY);
    this.setLineAttributes(this.bottomDrawingQueryStr, minX, maxY, maxX, maxY);
    this.setLineAttributes(this.leftDrawingQueryStr, minX, minY, minX, maxY);
    this.setLineAttributes(this.rightDrawingQueryStr, maxX, minY, maxX, maxY);
    this.setLineAttributes(this.topActionTriggerQueryStr, minX, minY, maxX, minY);
    this.setLineAttributes(this.bottomActionTriggerQueryStr, minX, maxY, maxX, maxY);
    this.setLineAttributes(this.leftActionTriggerQueryStr, minX, minY, minX, maxY);
    this.setLineAttributes(this.rightActionTriggerQueryStr, maxX, minY, maxX, maxY);

    // Adjust resizing border circles.
    this.setCircleAttributes(this.topLeftDrawingQueryStr, minX, minY);
    this.setCircleAttributes(this.topRightDrawingQueryStr, maxX, minY);
    this.setCircleAttributes(this.bottomLeftDrawingQueryStr, minX, maxY);
    this.setCircleAttributes(this.bottomRightDrawingQueryStr, maxX, maxY);
    this.setCircleAttributes(this.topLeftActionTriggerQueryStr, minX, minY);
    this.setCircleAttributes(this.topRightActionTriggerQueryStr, maxX, minY);
    this.setCircleAttributes(this.bottomLeftActionTriggerQueryStr, minX, maxY);
    this.setCircleAttributes(this.bottomRightActionTriggerQueryStr, maxX, maxY);
}

MoveAndResizeElementWrapper.prototype.setRectangleAttributes = function (rectQueryStr, x, y, width, height) {
    var rectElem = $(rectQueryStr);
    rectElem.attr('x', x);
    rectElem.attr('y', y);
    rectElem.attr('width', width);
    rectElem.attr('height', height);
}

MoveAndResizeElementWrapper.prototype.setLineAttributes = function (lineQueryStr, x1, y1, x2, y2) {
    var lineElem = $(lineQueryStr);
    lineElem.attr('x1', x1);
    lineElem.attr('y1', y1);
    lineElem.attr('x2', x2);
    lineElem.attr('y2', y2);
}

MoveAndResizeElementWrapper.prototype.setCircleAttributes = function (circleQueryStr, cx, cy) {
    var circleElem = $(circleQueryStr);
    circleElem.attr('cx', cx);
    circleElem.attr('cy', cy);
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Israel Israel
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions