Если вы пересматриваете этот вопрос, я переместил все обновления снизу, чтобы он действительно лучше читался в качестве вопроса.
Проблема
У меня есть немного странная проблема с обработкой событий браузера с помощью D3
. К сожалению, это сидит в довольно большом приложении, и потому что я полностью потерял свою причину, я изо всех сил пытаюсь найти небольшой воспроизводимый пример, поэтому я собираюсь предоставить как можно больше полезной информации, как я могу.
Итак, моя проблема заключается в том, что события click
, похоже, не срабатывают надежно для определенных элементов DOM. У меня есть два разных набора элементов: Заполненные круги и Белые круги. Вы можете видеть на скриншоте ниже 1002 и 1003 белые круги, а поставщики - заполненный круг.
Теперь эта проблема только возникает для белых кругов, которые я не понимаю. Снимок экрана ниже показывает, что происходит, когда я нажимаю круги. Порядок кликов отображается с помощью красных цифр и связанного с ними журнала. По существу, вы видите:
- MouseDown
- MouseUp
- иногда щелчок
Проблема немного спорадическая. Мне удалось отследить реалистичное воспроизведение, но после нескольких обновлений браузера теперь гораздо сложнее воспроизвести. Если я чередую клик на 1002 и 1003, я продолжаю получать события mousedown
и mouseup
, но никогда не click
. Если я нажму на один из них во второй раз, я получаю событие click
. Если я продолжаю нажимать на один и тот же (не показано здесь), только каждый другой клик запускает событие click
.
Если я повторяю тот же процесс с заполненным кругом, как Поставщики, тогда он отлично работает и click
запускается каждый раз.
Как создаются круги
Итак, круги (ака Планеты в моем коде) были созданы как модульный компонент. Там, где данные зацикливаются, и создается экземпляр для каждого из них.
data.enter()
.append("g")
.attr("class", function (d) { return d.promoted ? "collection moon-group" : "collection planet-group"; })
.call(drag)
.attr("transform", function (d) {
var scale = d.size / 150;
return "translate(" + [d.x, d.y] + ") scale(" + [scale] + ")";
})
.each(function (d) {
// Create a new planet for each item
d.planet = new d3.landscape.Planet()
.data(d, function () { return d.id; })
.append(this, d);
});
Это не говорит вам столько, что под графом Force Directed
используется для вычисления позиций. Код внутри функции Planet.append()
выглядит следующим образом:
d3.landscape.Planet.prototype.append = function (target) {
var self = this;
// Store the target for later
self.__container = target;
self.__events = new custom.d3.Events("planet")
.on("click", function (d) { self.__setSelection(d, !d.selected); })
.on("dblclick", function (d) { self.__setFocus(d, !d.focused); self.__setSelection(d, d.focused); });
// Add the circles
var circles = d3.select(target)
.append("circle")
.attr("data-name", function (d) { return d.name; })
.attr("class", function(d) { return d.promoted ? "moon" : "planet"; })
.attr("r", function () { return self.__animate ? 0 : self.__planetSize; })
.call(self.__events);
Здесь мы видим, что добавляемые круги (обратите внимание, что каждая Планета на самом деле представляет собой только один круг). Custom.d3.Events сконструирован и вызван для круга, который только что был добавлен в DOM. Этот код используется как для заполненных, так и для белых кругов, единственное отличие - небольшое изменение в классах. DOM, созданный для каждого, выглядит следующим образом:
Filled
<g class="collection planet-group" transform="translate(683.080338895066,497.948470463691) scale(0.6666666666666666,0.6666666666666666)">
<circle data-name="Suppliers" class="planet" r="150"></circle>
<text class="title" dy=".35em" style="font-size: 63.1578947368421px;">Suppliers</text>
</g>
Белый
<g class="collection moon-group" transform="translate(679.5720546510213,92.00957926233855) scale(0.6666666666666666,0.6666666666666666)">
<circle data-name="1002" class="moon" r="150"></circle>
<text class="title" dy=".35em" style="font-size: 75px;">1002</text>
</g>
Что делает custom.d3.events?
Идея заключается в том, чтобы предоставить более богатую систему событий, чем вы по умолчанию. Например, разрешая двойные щелчки (которые не запускают одиночные клики) и длинные клики и т.д.
Когда события вызывают с контейнером circle
, выполняется следующее: настройка некоторых событий raw
с использованием D3. Это не те, которые были подключены к функции Planet.append()
, потому что объект events
предоставляет собственную пользовательскую отправку. Это события, которые я использую для отладки/ведения журнала;
custom.d3.Events = function () {
var dispatch = d3.dispatch("click", "dblclick", "longclick", "mousedown", "mouseup", "mouseenter", "mouseleave", "mousemove", "drag");
var events = function(g) {
container = g;
// Register the raw events required
g.on("mousedown", mousedown)
.on("mouseenter", mouseenter)
.on("mouseleave", mouseleave)
.on("click", clicked)
.on("contextmenu", contextMenu)
.on("dblclick", doubleClicked);
return events;
};
// Return the bound events
return d3.rebind(events, dispatch, "on");
}
Итак, здесь я подключаюсь к нескольким событиям. Взглянув на них в обратном порядке:
нажмите
Функция щелчка устанавливается так, чтобы просто регистрировать значение, с которым мы имеем дело с
function clicked(d, i) {
console.log("clicked", d3.event.srcElement);
// don't really care what comes after
}
MouseUp
Функция mouseup по существу регистрирует и очищает некоторые глобальные объекты окна, которые будут обсуждаться далее.
function mouseup(d, i) {
console.log("mouseup", d3.event.srcElement);
dispose_window_events();
}
MouseDown
Функция mousedown немного сложнее, и я буду включать ее полностью. Он делает несколько вещей:
- Запускает консоль для консоли
- Устанавливает события окна (прокладывает мышью/мышь на объект окна), поэтому мышь может быть запущена, даже если мышь больше не находится в круге, вызвавшем mousedown
- Находит позицию мыши и вычисляет некоторые пороговые значения
- Устанавливает таймер, чтобы вызвать длинный клик
-
Запускает отправку mousedown, которая живет на объекте custom.d3.event.
function mousedown(d, i) { console.log("mousedown", d3.event.srcElement); var context = this; dragging = true; mouseDown = true; // Wire up events on the window setup_window_events(); // Record the initial position of the mouse down windowStartPosition = getWindowPosition(); position = getPosition(); // If two clicks happened far apart (but possibly quickly) then suppress the double click behaviour if (windowStartPosition && windowPosition) { var distance = mood.math.distanceBetween(windowPosition.x, windowPosition.y, windowStartPosition.x, windowStartPosition.y); supressDoubleClick = distance > moveThreshold; } windowPosition = windowStartPosition; // Set up the long press timer only if it has been subscribed to - because // we don't want to suppress normal clicks otherwise. if (events.on("longclick")) { longTimer = setTimeout(function () { longTimer = null; supressClick = true; dragging = false; dispatch.longclick.call(context, d, i, position); }, longClickTimeout); } // Trigger a mouse down event dispatch.mousedown.call(context, d, i); if(debug) { console.log(name + ": mousedown"); } }
Обновление 1
Я должен добавить, что я испытал это в Chrome, IE11 и Firefox (хотя это, кажется, самый надежный из браузеров).
К сожалению, после некоторого обновления и изменения кода/возврата я изо всех сил пытался получить надежное воспроизведение. Однако я заметил, что это нечетно, так как следующая последовательность может давать разные результаты:
- F5 Обновить браузер
- Нажмите
1002
Иногда это срабатывает mousedown
, mouseup
, а затем click
. В других случаях он пропускает click
. Кажется довольно странным, что эта проблема может возникать спорадически между двумя разными нагрузками одной и той же страницы.
Я также должен добавить, что я пробовал следующее:
- Вызвал
mousedown
для отказа и проверки того, чтоclick
по-прежнему срабатывает, чтобы гарантировать, что спорадическая ошибка вmousedown
не может вызвать проблему. Я могу подтвердить, чтоclick
запустит событие, если вmousedown
есть ошибка. - Пытался проверить сроки. Я сделал это, вставив длинный цикл блокировки в
mousedown
и подтверждая, что событияmouseup
иclick
будут срабатывать после значительной задержки. Таким образом, события действительно выглядят последовательно, как и следовало ожидать.
Обновление 2
Быстрое обновление после комментария @CoolBlue заключается в том, что добавление пространства имен в мои обработчики событий, похоже, не имеет никакого значения. Следующая проблема по-прежнему возникает спорадически:
var events = function(g) {
container = g;
// Register the raw events required
g.on("mousedown.test", mousedown)
.on("mouseenter.test", mouseenter)
.on("mouseleave.test", mouseleave)
.on("click.test", clicked)
.on("contextmenu.test", contextMenu)
.on("dblclick.test", doubleClicked);
return events;
};
Также css
- это то, о чем я еще не упоминал. Css должен быть схожим между двумя разными типами. Полный комплект показан ниже, в частности, point-events
установлены на none
только для метки в середине круга. Я позаботился о том, чтобы не нажимать на это для некоторых моих тестов, хотя, по-моему, это не имеет большого значения.
/* Mixins */
/* Comment here */
.collection .planet {
fill: #8bc34a;
stroke: #ffffff;
stroke-width: 2px;
stroke-dasharray: 0;
transition: stroke-width 0.25s;
-webkit-transition: stroke-width 0.25s;
}
.collection .title {
fill: #ffffff;
text-anchor: middle;
pointer-events: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
font-weight: normal;
}
.collection.related .planet {
stroke-width: 10px;
}
.collection.focused .planet {
stroke-width: 22px;
}
.collection.selected .planet {
stroke-width: 22px;
}
.moon {
fill: #ffffff;
stroke: #8bc34a;
stroke-width: 1px;
}
.moon-container .moon {
transition: stroke-width 1s;
-webkit-transition: stroke-width 1s;
}
.moon-container .moon:hover circle {
stroke-width: 3px;
}
.moon-container text {
fill: #8bc34a;
text-anchor: middle;
}
.collection.moon-group .title {
fill: #8bc34a;
text-anchor: middle;
pointer-events: none;
font-weight: normal;
}
.collection.moon-group .moon {
stroke-width: 3px;
transition: stroke-width 0.25s;
-webkit-transition: stroke-width 0.25s;
}
.collection.moon-group.related .moon {
stroke-width: 10px;
}
.collection.moon-group.focused .moon {
stroke-width: 22px;
}
.collection.moon-group.selected .moon {
stroke-width: 22px;
}
.moon:hover {
stroke-width: 3px;
}
Обновление 3
Итак, я пробовал решать разные вещи. Один из них заключается в изменении CSS, так что круги white
1002 и 1003 теперь используют один и тот же класс и, следовательно, тот же CSS, что и Поставщики, который работал. Вы можете увидеть изображение и CSS ниже как доказательство:
<g class="collection planet-group" transform="translate(1132.9999823040162,517.9999865702812) scale(0.6666666666666666,0.6666666666666666)">
<circle data-name="1003" class="planet" r="150"></circle>
<text class="title" dy=".35em" style="font-size: 75px;">1003</text>
</g>
Я также решил изменить код custom.d3.event
, так как это самый сложный бит eventing. Я разделил его прямо вниз, чтобы просто просто зарегистрировать:
var events = function(g) {
container = g;
// Register the raw events required
g.on("mousedown.test", function (d) { console.log("mousedown.test"); })
.on("click.test", function (d) { console.log("click.test"); });
return events;
};
Теперь кажется, что это еще не решило проблему. Ниже приводится трассировка (теперь я не уверен, почему я получаю два события click.test, которые запускаются каждый раз, - оцените, если кто-нибудь может это объяснить... но пока это воспринимается как норма). То, что вы видите, это то, что при выделенном ocassion click.test
не регистрировался, я должен был снова щелкнуть - следовательно, двойной mousedown.test
до того, как был зарегистрирован клик.
Обновление 4
Итак, после предложения от @CoolBlue я попытался изучить d3.behavior.drag
, который был настроен. Я попытался удалить проводку поведения перетаскивания, и после этого я не вижу никаких проблем - что может указывать на проблему. Это предназначено для того, чтобы круги можно было перетаскивать в пределах графика, ориентированного по силе. Поэтому я добавил некоторые записи в перетаскивание, чтобы я мог следить за тем, что происходит:
var drag = d3.behavior.drag()
.on("dragstart", function () { console.log("dragstart"); self.__dragstart(); })
.on("drag", function (d, x, y) { console.log("drag", d3.event.sourceEvent.x, d3.event.sourceEvent.y); self.__drag(d); })
.on("dragend", function (d) { console.log("dragend"); self.__dragend(d); });
Я также указал на базу кода D3
для события перетаскивания, в которой есть флаг suppressClick
. Поэтому я немного изменил это, чтобы убедиться, что это подавляет щелчок, который я ожидал.
return function (suppressClick) {
console.log("supressClick = ", suppressClick);
w.on(name, null);
...
}
Результаты этого были немного странными. Я объединил весь журнал, чтобы проиллюстрировать 4 разных примера:
- Синий: щелчок стрелял правильно, я заметил, что
suppressClick
был ложным. - Красный: щелчок не срабатывал, похоже, что я случайно вызвал движение, но
suppressClick
был все еще ложным. - Желтый: щелчок стрелял,
suppressClick
был все еще ложным, но произошел случайный ход. Я не знаю, почему это отличается от предыдущего красного. - Зеленый: я намеренно слегка переместился, щелкнув этот набор
suppressClick
на true, и щелчок не загорелся.
Обновление 5
Таким образом, углубляясь в код D3
немного больше, я действительно не могу объяснить несоответствия, которые я вижу в поведении, которое я подробно описал в обновлении 4. Я просто попробовал что-то другое, вне возможности увидеть если бы он сделал то, что я ожидал. В основном я заставляю D3
никогда подавлять щелчок. Итак, в перетащить событие
return function (suppressClick) {
console.log("supressClick = ", suppressClick);
suppressClick = false;
w.on(name, null);
...
}
После этого мне все же удалось получить сбой, что вызывает вопросы о том, действительно ли это флаг suppressClick, который вызывает его. Это также может объяснить несоответствия в консоли с помощью обновления # 4. Я также попытался подняться setTimeout(off, 0)
там, и это не помешало всем щелчкам стрелять, как я ожидал.
Поэтому я считаю, что это говорит о том, что suppressClick
на самом деле не проблема. Здесь консольный журнал как доказательство (и у меня также была двойная проверка коллеги, чтобы убедиться, что здесь ничего не пропало):
Обновить 6
Я нашел еще немного кода, который может иметь отношение к этой проблеме (но я не уверен на 100%). Когда я подключаюсь к d3.behavior.drag
, я использую следующее:
var drag = d3.behavior.drag()
.on("dragstart", function () { self.__dragstart(); })
.on("drag", function (d) { self.__drag(d); })
.on("dragend", function (d) { self.__dragend(d); });
Итак, я только что просмотрел функцию self.__dragstart()
и заметил d3.event.sourceEvent.stopPropagation();
. В этих функциях не так много (как правило, только запуск/остановка графика, направленного на усиление, и обновление позиций линий).
Мне интересно, может ли это повлиять на поведение кликов. Если я выберу этот stopPropagation
, то вся моя поверхность начнет кастрюлю, что нежелательно, чтобы, вероятно, не ответ, но может стать еще одним способом исследования.
Обновление 7
Один из возможных ярких выбросов, которые я забыл добавить к первоначальному вопросу. Визуализация также поддерживает масштабирование/панорамирование.
self.__zoom = d3.behavior
.zoom()
.scaleExtent([minZoom, maxZoom])
.on("zoom", function () { self.__zoomed(d3.event.translate, d3.event.scale); });
Теперь для реализации этого на самом деле есть большой прямоугольник над всем. Таким образом, мой верхний уровень svg
выглядит следующим образом:
<svg class="galaxy">
<g width="1080" height="1795">
<rect class="zoom" width="1080" height="1795" style="fill: none; pointer-events: all;"></rect>
<g class="galaxy-background" width="1080" height="1795" transform="translate(-4,21)scale(1)"></g>
<g class="galaxy-main" width="1080" height="1795" transform="translate(-4,21)scale(1)">
... all the circles are within here
</g>
</svg>
Я вспомнил об этом, когда отключил d3.event.sourceEvent.stopPropagation();
в обратном вызове для события drag
на d3.behaviour.drag
. Это остановило любые события щелчка, проходящие через мои круги, которые немного смутили меня, затем я вспомнил большой прямоугольник при проверке DOM. Я не совсем уверен, почему повторное включение распространения предотвращает нажатие на данный момент.