2

I've written a drag-and-drop SVG page that has to monitor mousemove events and update the page (stretching some connecting lines between rectangles that are being dragged around and drawing lines when I drag one rectangle across another, changing the connection points).

I understand that monitoring mousemove means responding to a very large number of events. Nonetheless, the responsiveness is blazingly fast in Chrome and Opera, and plenty good enough in Safari and IE, but it's unacceptably jerky in Firefox (current versions of all browsers under Windows 7 and all except IE under MacOS Mavericks).

Disabling add-ons has no effect. Profiling doesn't show any obvious bottlenecks, and the Chrome and Firefox profiles look similar. That page is at http://repertorium.obdurodon.org/dev/test.xhtml, if anyone cares to look, but the question is more about whether this

  1. A known Firefox problem with SVG DOM manipulation that has no known work-around until Mozilla updates the JavaScript engine;

  2. a known Firefox problem with a known "best practice" workaround; or

  3. likely to be developer error. Thanks for any guidance.

[Edit: added JavaScript in response to suggestion from @JosephtheDreamer. Trying to add even a reduced HTML/SVG file that would illustrate the problem wound up exceeding the StackOverflow 30,000-character length limit]

"use strict";
/*
 * Synopsis: Drag and drop support for plectograms
 * Developer: David J. Birnbaum, djbpitt@gmail.com http://www.obdurodon.org
 * Project: http://repertorium.obdurodon.org
 * Date: First version 2014-05-30
 * Last revised: 2014-06-22
 *
 * To do:
 * js:
 *   Wrap initialization in self-executing anonymous function (SEAF) to
 *     bypass addressing the window 'load' event explicitly:
 *     http://code.tutsplus.com/tutorials/key-principles-of-maintainable-javascript--net-25536
 * xhtml/svg:
 *   Adjust hierarchy of XQuery output to match mockup markup
 *
 * drag and drop based on http://dl.dropboxusercontent.com/u/169269/group_drag.svg
 * see also http://www.codedread.com/dragtest2.svg
 */
window.addEventListener('load', plectogram_init, false);
var djb = function () {
    return {
        adjustLinesAndColumns: function (how) {
            // only values should be 'fade' and 'restore'
            var lines = document.getElementsByTagName('line');
            var lineColor = how == 'fade' ? 'lightgray' : 'black';
            var columnOpacity = how == 'fade' ? .5 : 1;
            for (var i = 0; i < lines.length; i++) {
                lines[i].style.stroke = lineColor;
            }
            var columns = document.getElementsByClassName('draggable');
            for (i = 0; i < djb.columnCount; i++) {
                if (columns[i] !== djb.newG) {
                    columns[i].style.opacity = columnOpacity;
                }
            }
        },
        assignEventListeners: function (g) {
            // other listeners are on window, so that they can be trapped when the mouse races ahead
            g.getElementsByTagName('image')[0].addEventListener('mousedown', djb.startMove, false);
            window.addEventListener('mousemove', djb.moveIt, false);
            window.addEventListener('mouseup', djb.endMove, false);
        },
        buildDict: function (g) {
            // attach a dictionary to each <g> with text as key and vertical position as value
            // also adds .getXPos() and .setXPos() methods to <g> object
            g.contents = {};
            var columnCells = g.getElementsByTagName('g');
            for (var i = 0; i < columnCells.length; i++) {
                var cellText = columnCells[i].getElementsByTagName('text')[0].textContent;
                var cellYPos = columnCells[i].getElementsByTagName('rect')[0].getAttribute('y');
                g.contents[cellText] = cellYPos;
            }
            g.getXPos = function () {
                return parseInt(this.getAttribute('transform').slice(10, -1));
            };
            g.setXPos = function (X) {
                this.setAttribute('transform', 'translate(' + X + ')');
            };
        },
        createNewG: function (image) {
            djb.newG = image.parentNode.cloneNode(true);
            djb.buildDict(djb.newG);
            djb.newG.oldX = djb.newG.getXPos();
            image.parentNode.parentNode.appendChild(djb.newG);
            image.parentNode.parentNode.removeChild(image.parentNode);
        },
        drawLines: function () {
            var columns, key, x1, y1, x2, y2, linesG, newLine, i;
            columns = djb.htmlToArray(document.getElementsByClassName('draggable'));
            columns.sort(function (a, b) {
                var result = a.getXPos() - b.getXPos();
                return (result);
            }); // numerical sort by Xpos of column
            linesG = document.getElementById('lines');
            for (i = 1; i < djb.columnCount; i++) {
                x1 = columns[i].getXPos();
                x2 = columns[i - 1].getXPos() + djb.columnWidth;
                for (key in columns[i].contents) {
                    if (columns[i].contents.hasOwnProperty(key)) {
                        if (columns[i - 1].contents.hasOwnProperty(key)) {
                            y1 = parseInt(columns[i].contents[key]) + djb.columnMidHeight;
                            y2 = parseInt(columns[i - 1].contents[key]) + djb.columnMidHeight;
                            newLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
                            newLine.setAttribute('x1', x1);
                            newLine.setAttribute('y1', y1);
                            newLine.setAttribute('x2', x2);
                            newLine.setAttribute('y2', y2);
                            newLine.setAttribute('stroke', 'darkgray');
                            newLine.setAttribute('stroke-width', '2');
                            linesG.appendChild(newLine);
                        }
                    }
                }
            }
            djb.newG.oldX = djb.newG.getXPos();
        },
        endMove: function (evt) {
            evt.preventDefault();
            var i;
            window.removeEventListener('mousemove', djb.moveIt, false);
            window.removeEventListener('mouseup', djb.endMove, false);
            var columns = document.getElementsByClassName('draggable');
            var allNewColumnPositions = [];
            for (i = 0; i < djb.columnCount; i++) {
                allNewColumnPositions.push(columns[i].getXPos());
            }
            // numerical array sorting at http://www.w3schools.com/jsref/jsref_sort.asp
            for (i = 0; i < djb.columnCount; i++) {
                if (allNewColumnPositions.indexOf(djb.initialColumnPositions[i]) == -1) {
                    djb.newG.setXPos(djb.initialColumnPositions[i]);
                }
            }
            djb.eraseLines();
            djb.drawLines();
            djb.adjustLinesAndColumns('restore');
            djb.newG = null;
        },
        eraseLines: function () {
            // someday this will be easier: http://red-team-design.com/removing-an-element-with-plain-javascript-remove-method/
            // must work backwards: http://stackoverflow.com/questions/1457544/javascript-loop-only-applying-to-every-other-element
            var lines = document.getElementsByTagName('line');
            for (var i = lines.length - 1; i >= 0; i--) {
                lines[i].parentNode.removeChild(lines[i]);
            }
        },
        htmlToArray: function (htmlCollection) {
            return Array.prototype.slice.call(htmlCollection);
        },
        moveIt: function (evt) {
            evt.preventDefault();
            var oldObjectX = djb.newG.getXPos();
            var newObjectX = oldObjectX + evt.clientX - djb.mouseStartX;
            djb.newG.setXPos(newObjectX);
            djb.stretchLines();
            djb.mouseStartX = evt.clientX;
            if (newObjectX < djb.objectX - djb.spacing && newObjectX > '0') {
                djb.swapColumns('left');
            } else if (newObjectX > djb.objectX + djb.spacing && djb.objectX < djb.farRight) {
                djb.swapColumns('right');
            }
        },
        setXPos: function (g, Xpos) {
            g.setAttribute('transform', 'translate(' + Xpos + ')');
        },
        startMove: function (evt) {
            // global variable, used by djb.moveIt()
            djb.mouseStartX = evt.clientX;
            /*
             * clone clicked <g> into djb.newG and attach event listeners
             * svg has no concept of z height, so replace with clone at end of element sequence to avoid
             *   losing focus by becoming hidden by later elements
             */
            djb.createNewG(this); //image element that was clicked is the hook
            // djb.objectX is global, records starting position of drag
            djb.objectX = djb.newG.getXPos();
            djb.assignEventListeners(djb.newG);
            djb.adjustLinesAndColumns('fade');
        },
        stretchLines: function () {
            var newGX = djb.newG.getXPos();
            var lines = document.getElementsByTagName('line');
            for (var i = 0; i < lines.length; i++) {
                var x1 = lines[i].getAttribute('x1');
                var x2 = lines[i].getAttribute('x2');
                if (x1 == djb.objectX + djb.spacing || x1 == djb.newG.oldX + djb.spacing) {
                    lines[i].setAttribute('x2', newGX + djb.columnWidth);
                } else if (x2 == djb.objectX - djb.spacing + djb.columnWidth || x2 == (djb.newG.oldX - djb.spacing + djb.columnWidth)) {
                    lines[i].setAttribute('x1', newGX);
                }
            }
        },
        swapColumns: function (side) {
            var columns, neighbor;
            columns = djb.htmlToArray(document.getElementsByClassName('draggable'));
            columns.sort(function (a, b) {
                var result = a.getXPos() - b.getXPos();
                return result;
            }); // numerical sort by Xpos of column
            neighbor = columns[djb.objectX / djb.spacing - 1];
            if (side == 'left') {
                djb.objectX = djb.objectX - djb.spacing;
                neighbor.setXPos(neighbor.getXPos() + djb.spacing);
            } else {
                djb.objectX = djb.objectX + djb.spacing;
                neighbor.setXPos(neighbor.getXPos() - djb.spacing);
            }
            djb.eraseLines();
            djb.drawLines();
        },
        columnsHTML: document.getElementsByClassName('draggable'),
        dummy: null
    }
}();
function plectogram_init() {
    var images, i;
    djb.columns = djb.htmlToArray(djb.columnsHTML);
    for (i = 0; i < djb.columns.length; i++) {
        djb.buildDict(djb.columns[i]);
    }
    djb.columnCount = djb.columns.length;
    djb.columnHeight = parseInt(djb.columns[0].getElementsByTagName('g')[0].getElementsByTagName('rect')[0].getAttribute('height'));
    djb.columnMidHeight = djb.columnHeight / 2;
    djb.columnWidth = parseInt(djb.columns[0].getElementsByTagName('g')[0].getElementsByTagName('rect')[0].getAttribute('width'));
    djb.initialColumnPositions = [];
    for (i = 0; i < djb.columns.length; i++) {
        djb.initialColumnPositions.push(djb.columns[i].getXPos());
    }
    djb.spacing = djb.initialColumnPositions[1] - djb.initialColumnPositions[0];
    djb.farRight = djb.initialColumnPositions[djb.initialColumnPositions.length - 1];
    djb.newG = null;
    djb.mouseStartX = null;
    images = document.getElementsByTagName('image');
    for (i = 0; i < images.length; i++) {
        images[i].addEventListener('mousedown', djb.startMove, false);
    }
}
obdurodon
  • 51
  • 8
  • You should put your code in this post instead of a page that could possibly be gone in a week or two. – Joseph Jun 28 '14 at 14:53
  • 3) I think. evt.preventDefault(); is missing from the javascript moveIt function. – Robert Longson Jun 28 '14 at 14:53
  • @JosephtheDreamer: the code is a few hundred lines each for the HTML and the JavaScript files, which doesn't seem appropriate for a posting, although I'm sort of new here, so if my reluctance is misguided, please let me know and I'll post it. It doesn't work in a fiddle, which doesn't render the SVG. – obdurodon Jun 28 '14 at 15:05
  • @RobertLongson: Thank you for catching that! I've updated the JavaScript, but the Firefox performance remains very jerky. – obdurodon Jun 28 '14 at 15:06
  • @obdurodon If you don't post the code here (or even a minimalistic, reproducable version), the post will lose future value (and will probably be deleted, downvoted at best). – Joseph Jun 28 '14 at 15:08
  • @JosephtheDreamer: Thanks! I've edited to add the JavaScript (incorporating the suggestion from @RobertLongson), but even a stripped-down HTML/SVG example exceeded the StackOverflow 30,000-character ceiling. I agree, of course, about the importance of retaining future value, but I'm not sure how to do that with a language as verbose as SVG. Any advice? – obdurodon Jun 28 '14 at 15:40
  • The profile shows very little time spent in JavaScript or DOM for me; it's almost all text painting. @robertlongson do we not layerize SVG with transform attributes? – Boris Zbarsky Jun 29 '14 at 02:52
  • @BorisZbarsky yes we do layerize SVG with transforms. – Robert Longson Jun 29 '14 at 08:44
  • @BorisZbarsky: OP here. I'm very grateful for your willingness (and that of @RobertLongson) to look into the question, and I regret that "layerize SVG with transform attributes" is beyond my expertise. Would it be most useful for me to sit quietly on the sidelines for a moment? – obdurodon Jun 29 '14 at 21:45
  • @RobertLongson Then how come we're repainting the text? – Boris Zbarsky Jun 30 '14 at 19:04
  • @obdurodon Yeah, that question was for RobertLongson. – Boris Zbarsky Jun 30 '14 at 19:05

0 Answers0