(Ошибка Google Chrome?) Почему события pointerover и document pointerup иногда не срабатывают?

#javascript #dom-events #drag #pointer-events #lume

#javascript #dom-события #перетаскивание #события указателя #lume

Вопрос:

В следующем примере перетаскивание куба в другую ячейку сетки работает с первого или двух раз. Но если вы продолжите пытаться отпустить куб, а затем перетащите его в новую ячейку, вы обнаружите, что иногда это не работает.

Вы заметите, что, когда это не работает, document pointerup событие ‘s запускается не так, как мы ожидаем, и как только мы отпускаем указатель, куб продолжает двигаться, даже когда мы не перетаскиваем.

Код, который настраивает обработчики событий, начинается со строки 159 JS. Сделайте его полноэкранным на рабочем столе, он может плохо поместиться на мобильном экране. Не беспокойтесь о том, что делают пользовательские элементы, основная проблема заключается в том, что обработчики событий в конце, похоже, не работают должным образом.

(видео после примера)

РЕДАКТИРОВАТЬ: эта конкретная проблема, похоже, возникает только в Chromium (desktop), но я не сталкиваюсь с проблемой в Firefox (desktop). Возможно, это ошибка Chrome.

 {
    // Register the LUME HTML elements with the browser.
    LUME.useDefaultNames();

    const { Node, html, reactify, autorun } = LUME;

    class Cube extends Node {
        constructor(...args) {
            super(...args);
            reactify(this, ['cubeSize'])
        }

        cubeSize = 200;

        connectedCallback() {
            super.connectedCallback();

            // Keep it a cube.
            this.stop = autorun(() => {
                this.size = [this.cubeSize, this.cubeSize, this.cubeSize];
            });
        }

        disconnectedCallback() {
            super.disconnectedCallback();
            this.stop();
        }

        // prettier-ignore
        template = () =>  html`
            <lume-node size="0 0 0" align="0.5 0.5 0.5">
                <lume-node class="cubeFace" position=${() => [0, 0, this.cubeSize/2]}  rotation="0 0 0"   size=${() => [this.cubeSize, this.cubeSize, 0]} mount-point="0.5 0.5" align="0.5 0.5 0.5"></lume-node>
                <lume-node class="cubeFace" position=${() => [0, 0, -this.cubeSize/2]} rotation="0 180 0" size=${() => [this.cubeSize, this.cubeSize, 0]} mount-point="0.5 0.5" align="0.5 0.5 0.5"></lume-node>
                <lume-node class="cubeFace" position=${() => [-this.cubeSize/2, 0, 0]} rotation="0 -90 0" size=${() => [this.cubeSize, this.cubeSize, 0]} mount-point="0.5 0.5" align="0.5 0.5 0.5"></lume-node>
                <lume-node class="cubeFace" position=${() => [this.cubeSize/2, 0, 0]}  rotation="0 90 0"  size=${() => [this.cubeSize, this.cubeSize, 0]} mount-point="0.5 0.5" align="0.5 0.5 0.5"></lume-node>
                <lume-node class="cubeFace" position=${() => [0, -this.cubeSize/2, 0]} rotation="-90 0 0" size=${() => [this.cubeSize, this.cubeSize, 0]} mount-point="0.5 0.5" align="0.5 0.5 0.5"></lume-node>
                <lume-node class="cubeFace" position=${() => [0, this.cubeSize/2, 0]}  rotation="90 0 0"  size=${() => [this.cubeSize, this.cubeSize, 0]} mount-point="0.5 0.5" align="0.5 0.5 0.5"></lume-node>
            </lume-node>
        `

        css = `
            .cubeFace {
                background: linear-gradient(43deg, rgb(63, 94, 251) 0%, rgb(252, 70, 107) 100%);
            }
        `;
    }
    
    customElements.define('cube-', Cube)

    /**
     * @mixin
     * @class ChildWatcher -
     * A mixin class that gives your custom element a childrenChagnedCallback()
     * that runs any time your element's children have changed while your element
     * is connected into a live DOM tree.
     */
    function ChildWatcher(Base) {
        return class ChildWatcher extends Base {
            connectedCallback() {
                super.connectedCallback?.();

                // When children have changed, recompute the layout.
                this.observer = new MutationObserver(() =>
                    this.childrenChangedCallback?.()
                );
                this.observer.observe(this, { childList: true });
            }

            disconnectedCallback() {
                super.disconnectedCallback?.();
                this.observer.disconnect();
            }
        };
    }

    class GridLayout extends ChildWatcher(LUME.Node) {
        constructor(...args) {
            super(...args)
            reactify(this, ['rows', 'columns'])
        }

        // If rows or columns is not provided (both zero), then the default is a square grid
        // that fits all images (f.e. give 7 images, the grid rows and columns
        // would be 3 and 3, for a total of 9 cells, and two cells would be empty
        // in the last row).
        rows = 0;
        columns = 0;

        connectedCallback() {
            super.connectedCallback();

            // Run an initial layout on connect, and also recompute layout whenever this.rows or this.columns change.
            this.stop = LUME.autorun(() => {
                this.layout(this.rows, this.columns);
            });
        }

        disconnectedCallback() {
            // Don't forget cleanup!
            this.stop();
        }

        childrenChangedCallback() {
            // Recompute layout any time this element's children have changed
            // (it is batched, so happens once per macrotask, for better performance)
            this.layout(this.rows, this.columns);
        }

        layout(rows, columns) {
            // Calculate the grid rows and columns to be a square if rows/columns isn't provided.
            if (!rows || !columns) {
                const size = Math.ceil(Math.sqrt(this.children.length));
                rows = size;
                columns = size;
            }

            const cells = rows * columns;
            const gridSize = this.calculatedSize; // [x, y, z] in px units
            const cellWidth = gridSize.x / columns;
            const cellHeight = gridSize.y / rows;

            for (let i = 0; i < cells; i  = 1) {
                const node = this.children[i];

                // If we have less nodes than total cells, quit early.
                if (!node) break;

                node.size = [cellWidth, cellHeight];
                node.position = {
                    x: (i % columns) * cellWidth,
                    y: Math.floor(i / columns) * cellHeight
                };
            }
        }
    }
    
    customElements.define('grid-layout', GridLayout)
}

const cells = document.querySelectorAll("grid-layout > *");

// This event never fires
cells.forEach((n, i) => {
    n.addEventListener("gotpointercapture", (event) => {
        console.log(" --------------- pointer capture, uncapture it.", event.target);
        event.target.releasePointerCapture(event.pointerId);
    });
});

// This event also never fires
gridContainer.addEventListener("gotpointercapture", (event) => {
    console.log(" --------------- pointer capture, uncapture it.", event.target);
    event.target.releasePointerCapture(event.pointerId);
});

cube.addEventListener("pointerdown", (event) => {
    console.log(" --------------- pointer down, start things.");

    event.target.releasePointerCapture(event.pointerId);

    cube.classList.add("no-events");

    const handlers = [];

    cells.forEach((n, i) => {
        const handler = (event) => {
            console.log(" --------------- pointer over cell, move cube.");
            event.target.releasePointerCapture(event.pointerId);
            console.log("pointer over cell", i);
            cube.position = n.position;
        };
        handlers.push(handler);
        n.addEventListener("pointerover", handler);
    });

    document.addEventListener(
        "pointerup",
        (event) => {
            console.log(" --------------- pointer up, stop things.");
            cube.classList.remove("no-events");
            cells.forEach((n, i) => n.removeEventListener("pointerover", handlers[i]));
        },
        { once: true }
    );
}); 
 html,
body {
    width: 100%;
    height: 100%;
    margin: 0;
    background: #444;
}

grid-layout {
    outline: 4px solid #fc466b;
    touch-action: none;
    pointer-events: none;
}

grid-layout > * {
    outline: 1px solid #3f5efb;
    pointer-events: auto;
}

.no-events {
    pointer-events: none;
} 
 <!-- 
Made with LUME
http://github.com/lume/lume
-->
<script src="https://assets.codepen.io/191583/LUME.unversioned.2.js"></script>

<lume-scene id="scene" perspective="1000">

    <lume-node id="gridContainer" size="600 600" position="20 20" align="0.5 0.5" mount-point="0.5 0.5" rotation="10 30 0">

        <cube- class="Xno-events" id="cube" cubeSize="200" position="0 0"></cube->

        <!-- Make the grid fill 75% width and 75% height of the scene, and center it. -->
        <grid-layout id="grid" size-mode="proportional proportional" size="1 1">

            <!-- Put nine cells in the grid, so 3x3 grid by default -->
            <lume-node></lume-node>
            <lume-node></lume-node>
            <lume-node></lume-node>

            <lume-node></lume-node>
            <lume-node></lume-node>
            <lume-node></lume-node>

            <lume-node></lume-node>
            <lume-node></lume-node>
            <lume-node></lume-node>

        </grid-layout>
    </lume-node>

</lume-scene> 

Видео на YouTube показывает, что начальное перетаскивание работает хорошо. После того, как я отпустил мышь, я покачиваю мышью, чтобы сигнализировать, когда я закончил перетаскивание.

Затем я пытаюсь перетащить второй раз, но куб не перемещается. Как только я отпущу, и я больше не удерживаю кнопку мыши, куб начинает двигаться (он не должен двигаться, если кнопка мыши не нажата).

Причина, по которой куб перемещается, когда я не перетаскиваю, заключается в том, что document pointerup событие ‘s не срабатывало, когда я в последний раз останавливал перетаскивание, поэтому оно не удаляло pointerover события в ячейках.

Пока я пытался перетащить куб во второй раз, ни одно из pointerover событий ячейки не запускалось, пока я пытался перетащить (из-за вышеупомянутой проблемы). Эти события начали срабатывать только после того, как я отпустил кнопку мыши, но это неправильно, и когда я не перетаскиваю, не должно быть никакого движения куба.

Комментарии:

1. @user202729 Я нахожусь на рабочем столе Chrome (в Linux). Я обновил вопрос, включив ссылку на видео на YouTube, которое показывает, что происходит.

2. @user202729 Этот синтаксис называется «необязательной цепочкой». Это работает в моих версиях Chrome и Firefox. Вы используете более старую версию Chrome? Вот матрица поддержки для этого синтаксиса: caniuse.com/mdn-javascript_operators_optional_chaining

3. Может быть, это как-то связано с. pointercancel

4. @user202729 Спасибо! Позвольте мне разобраться в этом…

5. @user202729 События pointercancel действительно вызывают проблему. Команда Chromium проводит расследование в bugs.chromium.org/p/chromium/issues/detail?id=1166044 .