~glacambre/firenvim

ref: 5aef1b4350992faa00f763dce32fac404451c472 firenvim/src/page/functions.ts -rw-r--r-- 7.1 KiB
5aef1b43glacambre Enable shortcut to trigger firenvim 2 years ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import * as browser from "webextension-polyfill";
import { computeSelector } from "../utils/CSSUtils";

function executeInPage(code: string): Promise<any> {
    return new Promise((resolve, reject) => {
        const script = document.createElement("script");
        const eventId = (new URL(browser.runtime.getURL(""))).hostname + Math.random();
        script.innerHTML = `((evId) => {
            try {
                let result;
                result = ${code};
                window.dispatchEvent(new CustomEvent(evId, {
                    detail: {
                        success: true,
                        result,
                    }
                }));
            } catch (e) {
                window.dispatchEvent(new CustomEvent(evId, {
                    detail: { success: false, reason: e },
                }));
            }
        })(${JSON.stringify(eventId)})`;
        window.addEventListener(eventId, ({ detail }: any) => {
            script.parentNode.removeChild(script);
            if (detail.success) {
                return resolve(detail.result);
            }
            return reject(detail.reason);
        }, { once: true });
        document.head.appendChild(script);
    });
}

function _getElementContent(e: any): Promise<string> {
    if (e.className.match(/CodeMirror/gi)) {
        const selector = computeSelector(e);
        return executeInPage(`(${(selec: string) => {
            const elem = document.querySelector(selec) as any;
            return elem.CodeMirror.getValue();
        }})(${JSON.stringify(selector)})`);
    } else if (e.className.match(/ace_editor/gi)) {
        return executeInPage(`(${(selec: string) => {
            const elem = document.querySelector(selec) as any;
            return (window as any).ace.edit(elem).getValue();
        }})(${JSON.stringify(computeSelector(e))})`);
    }
    if (e.value !== undefined) {
        return Promise.resolve(e.value);
    }
    if (e.textContent !== undefined) {
        return Promise.resolve(e.textContent);
    }
    return Promise.resolve(e.innerText);
}

export function getFunctions(global: {
    lastEditorLocation: [string, string, number],
    nvimify: (evt: FocusEvent) => void,
    selectorToElems: Map<string, PageElements>,
    disabled: boolean | Promise<boolean>,
}) {
    return {
        forceNvimify: () => {
            let elem = document.activeElement;
            if (!elem || elem === document.documentElement || elem === document.body) {
                function isVisible(e: HTMLElement) {
                    const rect = e.getBoundingClientRect();
                    const viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
                    return !(rect.bottom < 0 || rect.top - viewHeight >= 0);
                }
                elem = Array.from(document.getElementsByTagName("textarea"))
                    .find(isVisible);
                if (!elem) {
                    elem = Array.from(document.getElementsByTagName("input"))
                        .find(e => e.type === "text" && isVisible(e));
                }
                if (!elem) {
                    return;
                }
            }
            global.nvimify({ target: elem } as any);
        },
        getEditorLocation: () => {
            // global.lastEditorLocation[1] is a selector. If no selector is
            // defined, we're not the script that should answer this question
            // and thus return a Promise that will never be resolved
            if (global.lastEditorLocation[1] === "") {
                // This cast is wrong but we need it in order to be able to
                // typecheck our proxy in page/proxy.ts. Note that it's ok
                // because the promise will never return anyway.
                return new Promise(() => undefined) as Promise<typeof global.lastEditorLocation>;
            }
            // We need to reset global.lastEditorLocation in order to avoid
            // accidentally giving an already-given selector if we receive a
            // message that isn't addressed to us. Note that this is a hack, a
            // proper fix would be depending on frameIDs, but we can't do that
            // efficiently
            const result = global.lastEditorLocation;
            global.lastEditorLocation = ["", "", 0];
            return Promise.resolve(result);
        },
        getElementContent: (selector: string) => _getElementContent(global.selectorToElems.get(selector).input),
        killEditor: (selector: string) => {
            const { span, input } = global.selectorToElems.get(selector);
            span.parentNode.removeChild(span);
            global.selectorToElems.delete(selector);
            input.removeEventListener("focus", global.nvimify);
            input.focus();
            input.addEventListener("focus", global.nvimify);
        },
        resizeEditor: (selector: string, width: number, height: number) => {
            const { iframe } = global.selectorToElems.get(selector);
            iframe.style.width = `${width}px`;
            iframe.style.height = `${height}px`;
        },
        setDisabled: (disabled: boolean) => {
            global.disabled = disabled;
        },
        setElementContent: (selector: string, text: string) => {
            const { input: e } = global.selectorToElems.get(selector) as any;
            if (e.className.match(/CodeMirror/gi)) {
                return executeInPage(`(${(selec: string, str: string) => {
                    const elem = document.querySelector(selec) as any;
                    return elem.CodeMirror.setValue(str);
                }})(${JSON.stringify(selector)}, ${JSON.stringify(text)})`);
            } else if (e.className.match(/ace_editor/gi)) {
                return executeInPage(`(${(selec: string, str: string) => {
                    const elem = document.querySelector(selec) as any;
                    return (window as any).ace.edit(elem).setValue(str);
                }})(${JSON.stringify(selector)}, ${JSON.stringify(text)})`);
            }
            if (e.value !== undefined) {
                e.value = text;
            } else {
                e.textContent = text;
            }
            e.dispatchEvent(new Event("keydown",     { bubbles: true }));
            e.dispatchEvent(new Event("keyup",       { bubbles: true }));
            e.dispatchEvent(new Event("keypress",    { bubbles: true }));
            e.dispatchEvent(new Event("beforeinput", { bubbles: true }));
            e.dispatchEvent(new Event("input",       { bubbles: true }));
            e.dispatchEvent(new Event("change",      { bubbles: true }));
        },
        setElementCursor: async (selector: string, line: number, column: number) => {
            const { input } = global.selectorToElems.get(selector) as any;
            if (!input.setSelectionRange) {
                return;
            }
            const pos = (await _getElementContent(input))
                .split("\n")
                .reduce((acc: number, l: string, index: number) => acc + (index < (line - 1)
                    ? (l.length + 1)
                    : 0), column + 1);
            input.setSelectionRange(pos, pos);
        },
    };
}