d3.js :在 dragstart/mousedown 期间重新插入元素会干扰 Chrome 和 Safari 中的点击事件,但不会干扰 Firefox
d3.js : re-inserting elements during dragstart/mousedown interferes with click events in Chrome and Safari but not Firefox
我 运行 使用 d3.js 在同一元素上实现单击和拖动功能时遇到问题...任何帮助将不胜感激。 Chrome 和 Safari 会出现下面描述的问题,但 Firefox 不会。
一些背景知识
当元素被拖动时(至少对于 d3)事件的顺序如下:
- 拖动开始
- 拖动
- 龙
- 点击
- dblclick
即使实际上没有拖动元素(因此没有拖动事件),dragstart 和 dragend 事件仍然会触发(在 mousedown 和 mouseup 上),随后会发生 click 事件。
问题
我需要在同一个 SVG 元素上同时具有拖动和单击(以及双击)功能。当拖动一个元素时,对我们来说很重要的一点是,该元素始终在 z-index 意义上层叠在任何其他元素之上(即,这样它在被拖动时不会被同级元素隐藏)。对于 SVG,DOM 中较晚出现的元素位于较早出现的元素之上。因此,为了保证拖动元素始终在顶部,我在执行 dragstart 事件处理程序期间将拖动元素附加为其父元素的最后一个子元素,使用:
elem.parentNode.appendChild(elem);
不幸的是,如果相关元素不是开始的最后一个子元素(因此随后被附加到其新的 DOM 地点)。如果该元素作为最后一个子元素开始(或从上一次点击附加到那里),则点击事件会在后续点击时触发。 mousedown 和 mouseup 事件也会发生同样的事情(在没有拖动行为的情况下),尽管这个用例对我们来说并不重要。同样,none 这是 Firefox 中的一个问题。
更复杂的情况(这里太难解释了)是我还需要在 dragend 事件完成后重新绘制元素,这还涉及将元素重新排序为它们的原始 DOM 顺序。因此,即使一个元素被点击一次,一旦 dragend 处理程序完成执行,它也永远不会成为最后一个子元素。
示范[=36=]
我这里设置了一个演示:http://jsfiddle.net/Lb4e6sss/3/
圆圈表示拖动行为的问题,方块表示 mousedown/mouseup 处理程序的问题。单击圆形或正方形应切换形状的颜色。请注意,在 Chrome 和 Safari 上,只有最后一个圆圈或方块会触发点击事件,而在 Firefox 上它们都会触发。
上面demo的JS代码如下:
var svg = d3.select('body > svg');
var radius = 30;
var circleData = [
{
position: { x: 90, y: 100 },
value: 1
},
{
position: { x: 220, y: 100 },
value: 2
},
{
position: { x: 350, y: 100 },
value: 3
}
];
var squareData = [
{
position: { x: 90, y: 350 },
value: 4
},
{
position: { x: 220, y: 350 },
value: 5
},
{
position: { x: 350, y: 350 },
value: 6
}
];
var setupCircles;
var setupSquares;
var reinsertElem = function (elem) {
elem.parentNode.appendChild(elem);
};
var handleClick = function (d, i) {
console.debug('clicked: ' + i);
// Make lit on click only!!
var shapeGroup = d3.select(this);
var isLit = shapeGroup.classed('lit');
shapeGroup.classed('lit', !isLit);
};
var handleMouseDown = function (d, i) {
console.debug('mouseDown: ' + i);
// Re-insert the clicked shape so that it appears on top of all other shapes
reinsertElem(this);
};
var handleMouseUp = function (d, i) {
console.debug('mouseUp: ' + i);
setupSquares();
};
var originFn = function (d) {
return d.position;
};
var dragStartFn = function (d, i) {
console.debug('drag started: ' + i);
// Prevent the event from bubbling up
d3.event['sourceEvent'].stopPropagation();
// Re-insert the drag shape so that it appears on top of all other shapes
reinsertElem(this);
};
var dragFn = function (d, i) {
// Constrain the drag position to keep the shape displayed in the rect area
var newX = Math.max(radius, Math.min(600 - radius, d3.event.x));
var newY = Math.max(radius, Math.min(250 - radius, d3.event.y));
circleData[i].position = { x: newX, y: newY };
d.position = { x: newX, y: newY };
d3.select(this).attr('transform', function (d) { return 'translate(' + newX + ',' + newY + ')'; });
};
var dragEndFn = function (d, i) {
console.debug('drag ended: ' + i);
setupCircles();
};
var dragBehavior = d3.behavior.drag()
.origin(originFn)
.on('dragstart', dragStartFn)
.on('drag', dragFn)
.on('dragend', dragEndFn);
setupCircles = function () {
var circlesGroup = svg.select('g.circles');
var circleGroup = circlesGroup.selectAll('g')
.data(circleData, function (d) { return d.value; });
var circleGroupEnter = circleGroup.enter()
.append('g')
.attr('class', function (d, i) { return 'circle' + i; })
.attr('transform', function (d) {
var position = d.position;
return 'translate(' + position.x + ',' + position.y + ')';
})
.on('click', handleClick)
.call(dragBehavior);
var circle = circleGroupEnter.append('circle')
.classed('fill0', true)
.classed('stroke1', true)
.attr('stroke-width', '1px')
.attr('r', radius.toString());
var text = circleGroupEnter.append('text')
.attr('font-family', 'Arial')
.attr('font-size', '20px')
.attr('dominant-baseline', 'central')
.attr('text-anchor', 'middle')
.text(function (d, i) { return d.value.toString(); });
circleGroup.order();
};
setupSquares = function () {
var squareWidth = radius * 2;
var squaresGroup = svg.select('g.squares');
var squareGroup = squaresGroup.selectAll('g')
.data(squareData, function (d) { return d.value; });
var squareGroupEnter = squareGroup.enter()
.append('g')
.attr('class', function (d, i) { return 'square' + i; })
.attr('transform', function (d) {
var positionX = d.position.x - (0.5 * squareWidth);
var positionY = d.position.y - (0.5 * squareWidth);
return 'translate(' + positionX + ',' + positionY + ')';
})
.on('click', handleClick)
.on('mousedown', handleMouseDown)
.on('mouseup', handleMouseUp);
var square = squareGroupEnter.append('rect')
.classed('fill0', true)
.classed('stroke1', true)
.attr('stroke-width', '1px')
.attr('width', squareWidth.toString())
.attr('height', squareWidth.toString());
var text = squareGroupEnter.append('text')
.attr('font-family', 'Arial')
.attr('font-size', '20px')
.attr('dominant-baseline', 'central')
.attr('text-anchor', 'middle')
.attr('x', function (d, i) { return (0.5 * squareWidth).toString(); })
.attr('y', function (d, i) { return (0.5 * squareWidth).toString(); })
.text(function (d, i) { return d.value.toString(); });
squareGroup.order();
};
setupCircles();
setupSquares();
任何关于如何解决这个问题的建议都将不胜感激。
我遇到了完全相同的问题。解决方案是:
- 添加一个布尔变量作为标志以指示需要重新插入。
- 在 dragStartFn 中,将标志设置为 true,并从此处删除重新插入位。
- 在 dragFn 中,在顶部添加一个检查,如果标志为真,则重新插入并将标志设置为假,这样它只发生一次。
这种方式对于直接点击不会发生重新插入并且事件被正确触发。然后对于实际拖动,重新插入仍然会发生,所以你们也都很好。
我 运行 使用 d3.js 在同一元素上实现单击和拖动功能时遇到问题...任何帮助将不胜感激。 Chrome 和 Safari 会出现下面描述的问题,但 Firefox 不会。
一些背景知识
当元素被拖动时(至少对于 d3)事件的顺序如下:
- 拖动开始
- 拖动
- 龙
- 点击
- dblclick
即使实际上没有拖动元素(因此没有拖动事件),dragstart 和 dragend 事件仍然会触发(在 mousedown 和 mouseup 上),随后会发生 click 事件。
问题
我需要在同一个 SVG 元素上同时具有拖动和单击(以及双击)功能。当拖动一个元素时,对我们来说很重要的一点是,该元素始终在 z-index 意义上层叠在任何其他元素之上(即,这样它在被拖动时不会被同级元素隐藏)。对于 SVG,DOM 中较晚出现的元素位于较早出现的元素之上。因此,为了保证拖动元素始终在顶部,我在执行 dragstart 事件处理程序期间将拖动元素附加为其父元素的最后一个子元素,使用:
elem.parentNode.appendChild(elem);
不幸的是,如果相关元素不是开始的最后一个子元素(因此随后被附加到其新的 DOM 地点)。如果该元素作为最后一个子元素开始(或从上一次点击附加到那里),则点击事件会在后续点击时触发。 mousedown 和 mouseup 事件也会发生同样的事情(在没有拖动行为的情况下),尽管这个用例对我们来说并不重要。同样,none 这是 Firefox 中的一个问题。
更复杂的情况(这里太难解释了)是我还需要在 dragend 事件完成后重新绘制元素,这还涉及将元素重新排序为它们的原始 DOM 顺序。因此,即使一个元素被点击一次,一旦 dragend 处理程序完成执行,它也永远不会成为最后一个子元素。
示范[=36=]
我这里设置了一个演示:http://jsfiddle.net/Lb4e6sss/3/
圆圈表示拖动行为的问题,方块表示 mousedown/mouseup 处理程序的问题。单击圆形或正方形应切换形状的颜色。请注意,在 Chrome 和 Safari 上,只有最后一个圆圈或方块会触发点击事件,而在 Firefox 上它们都会触发。
上面demo的JS代码如下:
var svg = d3.select('body > svg');
var radius = 30;
var circleData = [
{
position: { x: 90, y: 100 },
value: 1
},
{
position: { x: 220, y: 100 },
value: 2
},
{
position: { x: 350, y: 100 },
value: 3
}
];
var squareData = [
{
position: { x: 90, y: 350 },
value: 4
},
{
position: { x: 220, y: 350 },
value: 5
},
{
position: { x: 350, y: 350 },
value: 6
}
];
var setupCircles;
var setupSquares;
var reinsertElem = function (elem) {
elem.parentNode.appendChild(elem);
};
var handleClick = function (d, i) {
console.debug('clicked: ' + i);
// Make lit on click only!!
var shapeGroup = d3.select(this);
var isLit = shapeGroup.classed('lit');
shapeGroup.classed('lit', !isLit);
};
var handleMouseDown = function (d, i) {
console.debug('mouseDown: ' + i);
// Re-insert the clicked shape so that it appears on top of all other shapes
reinsertElem(this);
};
var handleMouseUp = function (d, i) {
console.debug('mouseUp: ' + i);
setupSquares();
};
var originFn = function (d) {
return d.position;
};
var dragStartFn = function (d, i) {
console.debug('drag started: ' + i);
// Prevent the event from bubbling up
d3.event['sourceEvent'].stopPropagation();
// Re-insert the drag shape so that it appears on top of all other shapes
reinsertElem(this);
};
var dragFn = function (d, i) {
// Constrain the drag position to keep the shape displayed in the rect area
var newX = Math.max(radius, Math.min(600 - radius, d3.event.x));
var newY = Math.max(radius, Math.min(250 - radius, d3.event.y));
circleData[i].position = { x: newX, y: newY };
d.position = { x: newX, y: newY };
d3.select(this).attr('transform', function (d) { return 'translate(' + newX + ',' + newY + ')'; });
};
var dragEndFn = function (d, i) {
console.debug('drag ended: ' + i);
setupCircles();
};
var dragBehavior = d3.behavior.drag()
.origin(originFn)
.on('dragstart', dragStartFn)
.on('drag', dragFn)
.on('dragend', dragEndFn);
setupCircles = function () {
var circlesGroup = svg.select('g.circles');
var circleGroup = circlesGroup.selectAll('g')
.data(circleData, function (d) { return d.value; });
var circleGroupEnter = circleGroup.enter()
.append('g')
.attr('class', function (d, i) { return 'circle' + i; })
.attr('transform', function (d) {
var position = d.position;
return 'translate(' + position.x + ',' + position.y + ')';
})
.on('click', handleClick)
.call(dragBehavior);
var circle = circleGroupEnter.append('circle')
.classed('fill0', true)
.classed('stroke1', true)
.attr('stroke-width', '1px')
.attr('r', radius.toString());
var text = circleGroupEnter.append('text')
.attr('font-family', 'Arial')
.attr('font-size', '20px')
.attr('dominant-baseline', 'central')
.attr('text-anchor', 'middle')
.text(function (d, i) { return d.value.toString(); });
circleGroup.order();
};
setupSquares = function () {
var squareWidth = radius * 2;
var squaresGroup = svg.select('g.squares');
var squareGroup = squaresGroup.selectAll('g')
.data(squareData, function (d) { return d.value; });
var squareGroupEnter = squareGroup.enter()
.append('g')
.attr('class', function (d, i) { return 'square' + i; })
.attr('transform', function (d) {
var positionX = d.position.x - (0.5 * squareWidth);
var positionY = d.position.y - (0.5 * squareWidth);
return 'translate(' + positionX + ',' + positionY + ')';
})
.on('click', handleClick)
.on('mousedown', handleMouseDown)
.on('mouseup', handleMouseUp);
var square = squareGroupEnter.append('rect')
.classed('fill0', true)
.classed('stroke1', true)
.attr('stroke-width', '1px')
.attr('width', squareWidth.toString())
.attr('height', squareWidth.toString());
var text = squareGroupEnter.append('text')
.attr('font-family', 'Arial')
.attr('font-size', '20px')
.attr('dominant-baseline', 'central')
.attr('text-anchor', 'middle')
.attr('x', function (d, i) { return (0.5 * squareWidth).toString(); })
.attr('y', function (d, i) { return (0.5 * squareWidth).toString(); })
.text(function (d, i) { return d.value.toString(); });
squareGroup.order();
};
setupCircles();
setupSquares();
任何关于如何解决这个问题的建议都将不胜感激。
我遇到了完全相同的问题。解决方案是:
- 添加一个布尔变量作为标志以指示需要重新插入。
- 在 dragStartFn 中,将标志设置为 true,并从此处删除重新插入位。
- 在 dragFn 中,在顶部添加一个检查,如果标志为真,则重新插入并将标志设置为假,这样它只发生一次。
这种方式对于直接点击不会发生重新插入并且事件被正确触发。然后对于实际拖动,重新插入仍然会发生,所以你们也都很好。