"use strict";
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.Recorder = void 0;
const selectorGenerator_1 = require("./selectorGenerator");
const html_1 = require("./html");
const scriptSymbol = Symbol('scriptSymbol');
class Recorder {
    constructor(injectedScript) {
        this._performingAction = false;
        this._highlightElements = [];
        this._listeners = [];
        this._hoveredModel = null;
        this._hoveredElement = null;
        this._activeModel = null;
        this._expectProgrammaticKeyUp = false;
        this._injectedScript = injectedScript;
        this._outerGlassPaneElement = html_1.html `
      <x-pw-glass style="
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        z-index: 2147483647;
        pointer-events: none;
        display: flex;
      ">
      </x-pw-glass>`;
        this._tooltipElement = html_1.html `<x-pw-tooltip></x-pw-tooltip>`;
        this._innerGlassPaneElement = html_1.html `
      <x-pw-glass-inner style="flex: auto">
        ${this._tooltipElement}
      </x-pw-glass-inner>`;
        // Use a closed shadow root to prevent selectors matching our internal previews.
        this._glassPaneShadow = this._outerGlassPaneElement.attachShadow({ mode: 'closed' });
        this._glassPaneShadow.appendChild(this._innerGlassPaneElement);
        this._glassPaneShadow.appendChild(html_1.html `
      <style>
        x-pw-tooltip {
          align-items: center;
          backdrop-filter: blur(5px);
          background-color: rgba(0, 0, 0, 0.7);
          border-radius: 2px;
          box-shadow: rgba(0, 0, 0, 0.1) 0px 3.6px 3.7px,
                      rgba(0, 0, 0, 0.15) 0px 12.1px 12.3px,
                      rgba(0, 0, 0, 0.1) 0px -2px 4px,
                      rgba(0, 0, 0, 0.15) 0px -12.1px 24px,
                      rgba(0, 0, 0, 0.25) 0px 54px 55px;
          color: rgb(204, 204, 204);
          display: none;
          font-family: 'Dank Mono', 'Operator Mono', Inconsolata, 'Fira Mono',
                       'SF Mono', Monaco, 'Droid Sans Mono', 'Source Code Pro', monospace;
          font-size: 12.8px;
          font-weight: normal;
          left: 0;
          line-height: 1.5;
          max-width: 600px;
          padding: 3.2px 5.12px 3.2px;
          position: absolute;
          top: 0;
        }
    </style>
    `);
        setInterval(() => {
            this._refreshListenersIfNeeded();
        }, 100);
    }
    _refreshListenersIfNeeded() {
        if (document.documentElement[scriptSymbol])
            return;
        document.documentElement[scriptSymbol] = true;
        removeEventListeners(this._listeners);
        this._listeners = [
            addEventListener(document, 'click', event => this._onClick(event), true),
            addEventListener(document, 'input', event => this._onInput(event), true),
            addEventListener(document, 'keydown', event => this._onKeyDown(event), true),
            addEventListener(document, 'keyup', event => this._onKeyUp(event), true),
            addEventListener(document, 'mousedown', event => this._onMouseDown(event), true),
            addEventListener(document, 'mouseup', event => this._onMouseUp(event), true),
            addEventListener(document, 'mousemove', event => this._onMouseMove(event), true),
            addEventListener(document, 'mouseleave', event => this._onMouseLeave(event), true),
            addEventListener(document, 'focus', () => this._onFocus(), true),
            addEventListener(document, 'scroll', () => {
                this._hoveredModel = null;
                this._updateHighlight();
            }, true),
        ];
        document.documentElement.appendChild(this._outerGlassPaneElement);
        if (window._recorderScriptReadyForTest)
            window._recorderScriptReadyForTest();
    }
    _actionInProgress(event) {
        // If Playwright is performing action for us, bail.
        if (this._performingAction)
            return true;
        // Consume as the first thing.
        consumeEvent(event);
        return false;
    }
    _consumedDueToNoModel(event, model) {
        if (model)
            return false;
        consumeEvent(event);
        return true;
    }
    _consumedDueWrongTarget(event) {
        if (this._activeModel && this._activeModel.elements[0] === deepEventTarget(event))
            return false;
        consumeEvent(event);
        return true;
    }
    _onClick(event) {
        if (this._shouldIgnoreMouseEvent(event))
            return;
        if (this._actionInProgress(event))
            return;
        if (this._consumedDueToNoModel(event, this._hoveredModel))
            return;
        const checkbox = asCheckbox(deepEventTarget(event));
        if (checkbox) {
            // Interestingly, inputElement.checked is reversed inside this event handler.
            this._performAction({
                name: checkbox.checked ? 'check' : 'uncheck',
                selector: this._hoveredModel.selector,
                signals: [],
            });
            return;
        }
        this._performAction({
            name: 'click',
            selector: this._hoveredModel.selector,
            signals: [],
            button: buttonForEvent(event),
            modifiers: modifiersForEvent(event),
            clickCount: event.detail
        });
    }
    _shouldIgnoreMouseEvent(event) {
        const target = deepEventTarget(event);
        const nodeName = target.nodeName;
        if (nodeName === 'SELECT')
            return true;
        if (nodeName === 'INPUT' && ['date'].includes(target.type))
            return true;
        return false;
    }
    _onMouseDown(event) {
        if (this._shouldIgnoreMouseEvent(event))
            return;
        if (!this._performingAction)
            consumeEvent(event);
        this._activeModel = this._hoveredModel;
    }
    _onMouseUp(event) {
        if (this._shouldIgnoreMouseEvent(event))
            return;
        if (!this._performingAction)
            consumeEvent(event);
    }
    _onMouseMove(event) {
        const target = deepEventTarget(event);
        if (this._hoveredElement === target)
            return;
        this._hoveredElement = target;
        // Mouse moved -> mark last action as committed via committing a commit action.
        this._commitActionAndUpdateModelForHoveredElement();
    }
    _onMouseLeave(event) {
        // Leaving iframe.
        if (deepEventTarget(event).nodeType === Node.DOCUMENT_NODE) {
            this._hoveredElement = null;
            this._commitActionAndUpdateModelForHoveredElement();
        }
    }
    _onFocus() {
        const activeElement = deepActiveElement(document);
        const result = activeElement ? selectorGenerator_1.generateSelector(this._injectedScript, activeElement) : null;
        this._activeModel = result && result.selector ? result : null;
        if (window._highlightUpdatedForTest)
            window._highlightUpdatedForTest(result ? result.selector : null);
    }
    _commitActionAndUpdateModelForHoveredElement() {
        if (!this._hoveredElement) {
            this._hoveredModel = null;
            this._updateHighlight();
            return;
        }
        const hoveredElement = this._hoveredElement;
        const { selector, elements } = selectorGenerator_1.generateSelector(this._injectedScript, hoveredElement);
        if ((this._hoveredModel && this._hoveredModel.selector === selector) || this._hoveredElement !== hoveredElement)
            return;
        window.playwrightRecorderCommitAction();
        this._hoveredModel = selector ? { selector, elements } : null;
        this._updateHighlight();
        if (window._highlightUpdatedForTest)
            window._highlightUpdatedForTest(selector);
    }
    _updateHighlight() {
        const elements = this._hoveredModel ? this._hoveredModel.elements : [];
        // Code below should trigger one layout and leave with the
        // destroyed layout.
        // Destroy the layout
        this._tooltipElement.textContent = this._hoveredModel ? this._hoveredModel.selector : '';
        this._tooltipElement.style.top = '0';
        this._tooltipElement.style.left = '0';
        this._tooltipElement.style.display = 'flex';
        // Trigger layout.
        const boxes = elements.map(e => e.getBoundingClientRect());
        const tooltipWidth = this._tooltipElement.offsetWidth;
        const tooltipHeight = this._tooltipElement.offsetHeight;
        const totalWidth = this._innerGlassPaneElement.offsetWidth;
        const totalHeight = this._innerGlassPaneElement.offsetHeight;
        // Destroy the layout again.
        if (boxes.length) {
            const primaryBox = boxes[0];
            let anchorLeft = primaryBox.left;
            if (anchorLeft + tooltipWidth > totalWidth - 5)
                anchorLeft = totalWidth - tooltipWidth - 5;
            let anchorTop = primaryBox.bottom + 5;
            if (anchorTop + tooltipHeight > totalHeight - 5) {
                // If can't fit below, either position above...
                if (primaryBox.top > tooltipHeight + 5) {
                    anchorTop = primaryBox.top - tooltipHeight - 5;
                }
                else {
                    // Or on top in case of large element
                    anchorTop = totalHeight - 5 - tooltipHeight;
                }
            }
            this._tooltipElement.style.top = anchorTop + 'px';
            this._tooltipElement.style.left = anchorLeft + 'px';
        }
        else {
            this._tooltipElement.style.display = 'none';
        }
        const pool = this._highlightElements;
        this._highlightElements = [];
        for (const box of boxes) {
            const highlightElement = pool.length ? pool.shift() : this._createHighlightElement();
            highlightElement.style.borderColor = this._highlightElements.length ? 'hotpink' : '#8929ff';
            highlightElement.style.left = box.x + 'px';
            highlightElement.style.top = box.y + 'px';
            highlightElement.style.width = box.width + 'px';
            highlightElement.style.height = box.height + 'px';
            highlightElement.style.display = 'block';
            this._highlightElements.push(highlightElement);
        }
        for (const highlightElement of pool) {
            highlightElement.style.display = 'none';
            this._highlightElements.push(highlightElement);
        }
    }
    _createHighlightElement() {
        const highlightElement = html_1.html `
      <x-pw-highlight style="
        position: absolute;
        top: 0;
        left: 0;
        width: 0;
        height: 0;
        border: 1px solid;
        box-sizing: border-box;">
      </x-pw-highlight>`;
        this._glassPaneShadow.appendChild(highlightElement);
        return highlightElement;
    }
    _onInput(event) {
        const target = deepEventTarget(event);
        if (['INPUT', 'TEXTAREA'].includes(target.nodeName)) {
            const inputElement = target;
            const elementType = (inputElement.type || '').toLowerCase();
            if (elementType === 'checkbox') {
                // Checkbox is handled in click, we can't let input trigger on checkbox - that would mean we dispatched click events while recording.
                return;
            }
            if (elementType === 'file') {
                window.playwrightRecorderRecordAction({
                    name: 'setInputFiles',
                    selector: this._activeModel.selector,
                    signals: [],
                    files: [...(inputElement.files || [])].map(file => file.name),
                });
                return;
            }
            // Non-navigating actions are simply recorded by Playwright.
            if (this._consumedDueWrongTarget(event))
                return;
            window.playwrightRecorderRecordAction({
                name: 'fill',
                selector: this._activeModel.selector,
                signals: [],
                text: inputElement.value,
            });
        }
        if (target.nodeName === 'SELECT') {
            const selectElement = target;
            if (this._actionInProgress(event))
                return;
            this._performAction({
                name: 'select',
                selector: this._hoveredModel.selector,
                options: [...selectElement.selectedOptions].map(option => option.value),
                signals: []
            });
        }
    }
    _shouldGenerateKeyPressFor(event) {
        // Backspace, Delete are changing input, will handle it there.
        if (['Backspace', 'Delete'].includes(event.key))
            return false;
        // Ignore the QWERTZ shortcut for creating a at sign on MacOS
        if (event.key === '@' && event.code === 'KeyL')
            return false;
        // Allow and ignore common used shortcut for pasting.
        if (navigator.platform.includes('Mac')) {
            if (event.key === 'v' && event.metaKey)
                return false;
        }
        else {
            if (event.key === 'v' && event.ctrlKey)
                return false;
            if (event.key === 'Insert' && event.shiftKey)
                return false;
        }
        if (['Shift', 'Control', 'Meta', 'Alt'].includes(event.key))
            return false;
        const hasModifier = event.ctrlKey || event.altKey || event.metaKey;
        if (event.key.length === 1 && !hasModifier)
            return !!asCheckbox(deepEventTarget(event));
        return true;
    }
    _onKeyDown(event) {
        if (!this._shouldGenerateKeyPressFor(event))
            return;
        if (this._actionInProgress(event)) {
            this._expectProgrammaticKeyUp = true;
            return;
        }
        if (this._consumedDueWrongTarget(event))
            return;
        // Similarly to click, trigger checkbox on key event, not input.
        if (event.key === ' ') {
            const checkbox = asCheckbox(deepEventTarget(event));
            if (checkbox) {
                this._performAction({
                    name: checkbox.checked ? 'uncheck' : 'check',
                    selector: this._activeModel.selector,
                    signals: [],
                });
                return;
            }
        }
        this._performAction({
            name: 'press',
            selector: this._activeModel.selector,
            signals: [],
            key: event.key,
            modifiers: modifiersForEvent(event),
        });
    }
    _onKeyUp(event) {
        if (!this._shouldGenerateKeyPressFor(event))
            return;
        // Only allow programmatic keyups, ignore user input.
        if (!this._expectProgrammaticKeyUp) {
            consumeEvent(event);
            return;
        }
        this._expectProgrammaticKeyUp = false;
    }
    async _performAction(action) {
        this._performingAction = true;
        await window.playwrightRecorderPerformAction(action);
        this._performingAction = false;
        // Action could have changed DOM, update hovered model selectors.
        this._commitActionAndUpdateModelForHoveredElement();
        // If that was a keyboard action, it similarly requires new selectors for active model.
        this._onFocus();
        if (window._actionPerformedForTest) {
            window._actionPerformedForTest({
                hovered: this._hoveredModel ? this._hoveredModel.selector : null,
                active: this._activeModel ? this._activeModel.selector : null,
            });
        }
    }
}
exports.Recorder = Recorder;
function deepEventTarget(event) {
    return event.composedPath()[0];
}
function deepActiveElement(document) {
    let activeElement = document.activeElement;
    while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement)
        activeElement = activeElement.shadowRoot.activeElement;
    return activeElement;
}
function modifiersForEvent(event) {
    return (event.altKey ? 1 : 0) | (event.ctrlKey ? 2 : 0) | (event.metaKey ? 4 : 0) | (event.shiftKey ? 8 : 0);
}
function buttonForEvent(event) {
    switch (event.which) {
        case 1: return 'left';
        case 2: return 'middle';
        case 3: return 'right';
    }
    return 'left';
}
function consumeEvent(e) {
    e.preventDefault();
    e.stopPropagation();
    e.stopImmediatePropagation();
}
function asCheckbox(node) {
    if (!node || node.nodeName !== 'INPUT')
        return null;
    const inputElement = node;
    return inputElement.type === 'checkbox' ? inputElement : null;
}
function addEventListener(target, eventName, listener, useCapture) {
    target.addEventListener(eventName, listener, useCapture);
    return { target, eventName, listener, useCapture };
}
function removeEventListeners(listeners) {
    for (const listener of listeners)
        listener.target.removeEventListener(listener.eventName, listener.listener, listener.useCapture);
    listeners.splice(0, listeners.length);
}
exports.default = Recorder;
//# sourceMappingURL=recorder.js.map