mercredi 29 avril 2020

Small Javascript Lib to draw SVG file

I'm new to JavaScript and I decided to make a script that allows other developers to show SVG elements in the web browser. (It is just for learning purpose), and it is still under development.

I code it in ES5 first and then I'm willing to upgrade it to ES6+ so I can learn and understand the difference.

I really appreciate your help to review it before I move forward and tell me what is wrong, what can I improve, if I used a design pattern in a wrong place... any help from you will be great.

So I tried to implement MVC pattern, inheritance, Factory pattern, function statement, function expression, IIFE ...

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script src="rasm.js"></script>
    <script src="app.js"></script>
</body>
</html>

src lib:

var modelController = function() {'use_strict';
    var createSVGElment = function(qualifiedName) {
        return document.createElementNS('http://www.w3.org/2000/svg', qualifiedName)
    };

    var Element = function(options) {
        this.element = null;
        this.style = options.style || '';
    };
    Element.prototype.buildElement = function() {
        this.element.setAttribute('style', this.style);
    };

    var Container = function(options) {
        Element.call(this, options);
        this.height = options.height || 700;
        this.width = options.width || 1200;
        this.element = createSVGElment('svg');
    };
    Container.prototype.buildElement = function() {
        Element.prototype.buildElement.call(this);
        this.element.setAttribute('width', this.width);
        this.element.setAttribute('height', this.height);
    };

    var Rectangle = function(options) {
        Element.call(this, options);
        this.height = options.height;
        this.width = options.width;
        this.x = options.x;
        this.y = options.y;
        this.text = options.text;
        this.element = createSVGElment('rect');
    };
    Rectangle.prototype.buildElement = function() {
        Element.prototype.buildElement.call(this);
        this.element.setAttribute('x', this.x);
        this.element.setAttribute('y', this.y);
        this.element.setAttribute('width', this.width);
        this.element.setAttribute('height', this.height);
    };

    var Ellipse = function(options) {
        Element.call(this, options);
        this.x = options.cx;
        this.y = options.cy;
        this.rx = options.rx;
        this.width = options.rx * 2;
        this.ry = options.ry;
        this.height = options.ry * 2;
        this.text = options.text;
        this.element = createSVGElment('ellipse');
    };
    Ellipse.prototype.buildElement = function() {
        Element.prototype.buildElement.call(this);
        this.element.setAttribute('cx', this.x);
        this.element.setAttribute('cy', this.y);
        this.element.setAttribute('rx', this.rx);
        this.element.setAttribute('ry', this.ry);
    };

    var Link = function(options) {
        Element.call(this, options);
        this.from = options.from;
        this.to = options.to;
        this.defsElement = createSVGElment('defs');
        this.element = createSVGElment('line');
        this.camputePath = function() {
            if (this.from instanceof Ellipse) {
                this.fromX = this.from.x + this.from.rx;
                this.fromY = this.from.y;
            } else {
                this.fromX = this.from.x + this.from.width;
                this.fromY = this.from.y + (this.from.height / 2);
            }

            if (this.to instanceof Ellipse) {
                this.toX = this.to.x - this.to.rx;
                this.toY = this.to.y;
            } else {
                this.toX = this.to.x;
                this.toY = this.to.y + (this.to.height / 2);
            }
        };
    };
    Link.prototype.buildElement = function() {
        var pathEelement = createSVGElment('path');
        pathEelement.setAttribute('d', 'M0,0 L0,6 L7,3 z');
        pathEelement.setAttribute('fill', '#000');

        var markerEelement = createSVGElment('marker');
        markerEelement.setAttribute('id', 'arrow');
        markerEelement.setAttribute('markerWidth', '10');
        markerEelement.setAttribute('markerHeight', '10');
        markerEelement.setAttribute('refX', '0');
        markerEelement.setAttribute('refY', '3');
        markerEelement.setAttribute('orient', 'auto');
        markerEelement.setAttribute('markerUnits', 'strokeWidth');

        markerEelement.appendChild(pathEelement);
        this.defsElement.appendChild(markerEelement);

        this.camputePath();
        this.element.setAttribute('x1', this.fromX);
        this.element.setAttribute('y1', this.fromY);
        this.element.setAttribute('x2', this.toX - 10);
        this.element.setAttribute('y2', this.toY);
        this.element.setAttribute('stroke', '#000');
        this.element.setAttribute('stroke-width', '2');
        this.element.setAttribute('marker-end', 'url(#arrow)');
    };

    var Text = function(options) {
        Element.call(this, options);
        this.x = options.x;
        this.y = options.y;
        this.value = options.value;
        this.element = createSVGElment('text');
    };
    Text.prototype.buildElement = function() {
        Element.prototype.buildElement.call(this);
        this.element.setAttribute('x', this.x);
        this.element.setAttribute('y', this.y);
        this.element.textContent = this.value;
    };

    // Element factory
    function ElementFactory() {};
    ElementFactory.prototype.createElement = function(o) {
        switch(o.type) {
            case 'container':
                this.elementClass = Container;
                break;
            case 'rect':
                this.elementClass = Rectangle;
                break;
            case 'ellipse':
                this.elementClass = Ellipse;
                break;
            case 'link':
                this.elementClass = Link;
                break;
            case 'text':
                this.elementClass = Text;
                break;
            default:
                throw 'Warning: the type ' + o.type + ' is invalid';
        }
        return new this.elementClass(o);
    };
    var elementFactory = new ElementFactory();

    // storing register
    var register = {
        eltSVG: null,
        elts: []
    };

    return {
        registerElement: function(options) {
            var el = elementFactory.createElement(options);
            if (options.type === 'container') {
                register.eltSVG = el;
            } else {
                register.elts.push(el);
            }
            return el;
        },

        getRegister: function() {
            return register;
        },
    };
}();

var viewController = function() {'use_strict';
    return {
        displayElement: function(el) {
            var selector = el.element.tagName === 'svg' ? 'body' : 'svg';
            el.buildElement();
            if (el.defsElement) { // for line element
                document.querySelector(selector).appendChild(el.defsElement);    
            }
            document.querySelector(selector).appendChild(el.element);
        },
    };
}();

(function(global, model, view) {'use_strict';
    function processText(el, o) {
        if (['above', 'inside', 'below'].indexOf(o.text.position) < 0) {
            throw "Text position must be: above, inside or below and not " + o.text.position;
        }
        var x, y;

        if (el.element.tagName === 'rect') {
            x = el.x + (el.width * 0.1) // 20% of the width
            if (o.text.position === 'above') {
                y = el.y - 5;
            } else if (o.text.position === 'inside') {
                y = el.y + (el.height / 2);
            } else if (o.text.position === 'below') {
                y = el.y + el.height + 20;
            }
        } else { // ellipse 
            x = el.x - (el.rx * 0.2)
            if (o.text.position === 'above') {
                y = el.y - el.ry - 5;
            } else if (o.text.position === 'inside') {
                y = el.y;
            } else if (o.text.position === 'below') {
                y = el.y + el.ry + 20;
            }
        }

        return {
            x: x,
            y: y,
        }
    };

    global.$R = global.Rasm = {
        draw: function(options) {
            if (options.type !== 'container' && !model.getRegister().eltSVG) {
                throw 'You must create an svg element first';
            }
            var el = model.registerElement(options);
            view.displayElement(el);

            // process text option
            if (options.text) {
                var textCoord = processText(el, options);
                var text = model.registerElement({
                    "type": "text",
                    "x": textCoord.x,
                    "y": textCoord.y,
                    "value": options.text.value,
                });
                view.displayElement(text);
            }
            return el;
        },
    };
})(window, modelController, viewController);

An example of how we can use it:

$R.draw({
    "type": "container",
    "height": 700,
    "width": 1100,
});

/** Left side */
var rect1 = $R.draw({
    "type": "rect",
    "x": 20,
    "y": 20,
    "width": 345,
    "height": 648,
    "style": "stroke-dasharray: 5.5; stroke: #006600; fill: #fff"
});
var rect2 = $R.draw({
    "type": "rect",
    "x": 50,
    "y": 100,
    "width": 250,
    "height": 100,
    "text": {
        "position": "inside",
        "value": "I'm a text inside a rectangle"
    },
    "style": "stroke: #006600; fill: #fff"
});
var rect3 = $R.draw({
    "type": "rect",
    "x": 50,
    "y": 300,
    "width": 250,
    "height": 100,
    "text": {
        "position": "above",
        "value": "I'm a text above a rectangle"
    },
    "style": "stroke: #006600; fill: #fff"
});
var rect4 = $R.draw({
    "type": "rect",
    "x": 50,
    "y": 500,
    "width": 250,
    "height": 100,
    "text": {
        "position": "below",
        "value": "I'm a text below a rectangle"
    },
    "style": "stroke: #006600; fill: #fff"
});

/** Right side */
var rect5 = $R.draw({
    "type": "rect",
    "x": 700,
    "y": 20,
    "width": 345,
    "height": 648,
    "style": "stroke-dasharray: 20; stroke: #006600; fill: #fff"
});
var ellipse1 = $R.draw({
    "type": "ellipse",
    "cx": 870,
    "cy": 200,
    "rx": 150,
    "ry": 70,
    "text": {
        "position": "above",
        "value": "I'm a text above an ellipse"
    },
    "style": "stroke: #129cc9; fill: #b5e5f5"
});

var ellipse2 = $R.draw({
    "type": "ellipse",
    "cx": 860,
    "cy": 500,
    "rx": 100,
    "ry": 70,
    "text": {
        "position": "inside",
        "value": "I'm a text inside an ellipse"
    },
    "style": "stroke: #129cc9; fill: #b5e5f5"
});

/** Links */
$R.draw({
    "type": "link",
    "from": rect2,
    "to": ellipse2,
});
$R.draw({
    "type": "link",
    "from": rect3,
    "to": ellipse1,
});
$R.draw({
    "type": "link",
    "from": rect4,
    "to": ellipse1,
});

I hope you concentrate only on the lib structure and code, the SVG example that I took is just to be able to practice what I have learned.

Aucun commentaire:

Enregistrer un commentaire