digitalfabrik/integreat-cms

View on GitHub
integreat_cms/static/src/js/tinymce-plugins/autolink_tel/plugin.js

Summary

Maintainability
A
0 mins
Test Coverage
/**
 * This is a fork of the original autolink plugin of the TinyMCE (see: https://github.com/tinymce/tinymce-dist/), which is licensed under the LGPL.
 *
 * The changes are aiming to add an automatic detection of phone numbers with the specific pattern of a leading zero and at least 5 more digits
 * that might be separated by a slash.
 *
 * Author of the changes: Jan-Ulrich Holtgrave (holtgrave@integreat-app.de)
 *
 * Copyright (c) Tiny Technologies, Inc. All rights reserved.
 * Licensed under the LGPL or a commercial license.
 * For LGPL see License.txt in the project root for license information.
 * For commercial licenses see https://www.tiny.cloud/
 *
 * Version: 5.7.0 (2021-02-10)
 */
(() => {
    const global = tinymce.util.Tools.resolve("tinymce.PluginManager");

    const global$1 = tinymce.util.Tools.resolve("tinymce.Env");

    const getAutoLinkPattern = (editor) =>
        editor.getParam(
            "autolink_pattern",
            /^(https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.|(?:mailto:)?[A-Z0-9._%+-]+@(?!.*@))(.+)$/i
        );
    const getDefaultLinkTarget = (editor) => editor.getParam("default_link_target", false);
    const getDefaultLinkProtocol = (editor) => editor.getParam("link_default_protocol", "http", "string");

    // constant values and magic numbers used below
    const CHAR_CODE_SPACE = 160;
    const CHAR_CODE_SOFT_HYPHEN = 173;
    const NODE_TYPE_ELEMENT = 1;
    const NODE_TYPE_TEXT = 3;
    const MIN_RANGE_THRESHOLD = 5;

    const rangeEqualsDelimiterOrSpace = (rangeString, delimiter) =>
        rangeString === delimiter || rangeString === " " || rangeString.charCodeAt(0) === CHAR_CODE_SPACE;
    const scopeIndex = (container, idx) => {
        let index = idx < 0 ? 0 : idx;
        if (container.nodeType === NODE_TYPE_TEXT) {
            const len = container.data.length;
            if (index > len) {
                index = len;
            }
        }
        return index;
    };
    const setStart = (rng, container, offset) => {
        if (container.nodeType !== NODE_TYPE_ELEMENT || container.hasChildNodes()) {
            rng.setStart(container, scopeIndex(container, offset));
        } else {
            rng.setStartBefore(container);
        }
    };
    const setEnd = (rng, container, offset) => {
        if (container.nodeType !== NODE_TYPE_ELEMENT || container.hasChildNodes()) {
            rng.setEnd(container, scopeIndex(container, offset));
        } else {
            rng.setEndAfter(container);
        }
    };
    const convertToInternational = (phoneNumber) => {
        const phoneNumberCleaned = phoneNumber.replace(/[-/]/g, "");

        let phoneNumberCallable;
        let phoneNumberBody;
        let countryCode = "+49";

        /* eslint-disable no-magic-numbers */
        /* eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check */
        switch (true) {
            case phoneNumber.startsWith("00"):
                phoneNumberCallable = `+${phoneNumberCleaned.substring(2)}`;
                phoneNumberBody = phoneNumberCleaned.substring(4);
                countryCode = `+${phoneNumberCleaned.substring(2, 4)}`;
                break;
            case phoneNumberCleaned.startsWith("0"):
                phoneNumberCallable = `${countryCode}${phoneNumberCleaned.substring(1)}`;
                phoneNumberBody = phoneNumberCleaned.substring(1);
                break;
            case phoneNumberCleaned.startsWith("+"):
                phoneNumberCallable = phoneNumberCleaned;
                phoneNumberBody = phoneNumberCleaned.substring(3);
                countryCode = phoneNumberCleaned.substring(0, 3);
                break;
        }
        /* eslint-enable no-magic-numbers */

        return [
            `<span class="notranslate" translate="no" dir="ltr">${countryCode} (0) ${phoneNumberBody}</span>`,
            `tel:${phoneNumberCallable}`,
        ];
    };
    const parseCurrentLine = (editor, endOffset, delimiter) => {
        let end;
        let endContainer;
        let bookmark;
        let text;
        let prev;
        let len;
        let rngText;
        const autoLinkPattern = getAutoLinkPattern(editor);
        const defaultLinkTarget = getDefaultLinkTarget(editor);
        if (editor.selection.getNode().tagName === "A") {
            return;
        }
        const rng = editor.selection.getRng().cloneRange();
        if (rng.startOffset < MIN_RANGE_THRESHOLD) {
            prev = rng.endContainer.previousSibling;
            if (!prev) {
                if (!rng.endContainer.firstChild || !rng.endContainer.firstChild.nextSibling) {
                    return;
                }
                prev = rng.endContainer.firstChild.nextSibling;
            }
            len = prev.length;
            setStart(rng, prev, len);
            setEnd(rng, prev, len);
            if (rng.endOffset < MIN_RANGE_THRESHOLD) {
                return;
            }
            end = rng.endOffset;
            endContainer = prev;
        } else {
            endContainer = rng.endContainer;
            if (endContainer.nodeType !== NODE_TYPE_TEXT && endContainer.firstChild) {
                while (endContainer.nodeType !== NODE_TYPE_TEXT && endContainer.firstChild) {
                    endContainer = endContainer.firstChild;
                }
                if (endContainer.nodeType === NODE_TYPE_TEXT) {
                    setStart(rng, endContainer, 0);
                    setEnd(rng, endContainer, endContainer.nodeValue.length);
                }
            }
            if (rng.endOffset === 1) {
                end = 2;
            } else {
                end = rng.endOffset - 1 - endOffset;
            }
        }
        const start = end;
        do {
            setStart(rng, endContainer, end >= 2 ? end - 2 : 0);
            setEnd(rng, endContainer, end >= 1 ? end - 1 : 0);
            end -= 1;
            rngText = rng.toString();
        } while (
            rngText !== " " &&
            rngText !== "" &&
            rngText.charCodeAt(0) !== CHAR_CODE_SPACE &&
            end - 2 >= 0 &&
            rngText !== delimiter
        );
        if (rangeEqualsDelimiterOrSpace(rng.toString(), delimiter)) {
            setStart(rng, endContainer, end);
            setEnd(rng, endContainer, start);
            end += 1;
        } else if (rng.startOffset === 0) {
            setStart(rng, endContainer, 0);
            setEnd(rng, endContainer, start);
        } else {
            setStart(rng, endContainer, end);
            setEnd(rng, endContainer, start);
        }
        text = rng.toString();
        if (text.charAt(text.length - 1) === ".") {
            setEnd(rng, endContainer, start - 1);
        }
        text = rng.toString().trim().replace(String.fromCharCode(CHAR_CODE_SOFT_HYPHEN), "");
        const matches = text.match(autoLinkPattern);
        const phoneMatches = text.match(/(0|\+)[0-9\-/]{6,20}/);
        const protocol = getDefaultLinkProtocol(editor);
        if (matches) {
            if (matches[1] === "www.") {
                matches[1] = `${protocol}://www.`;
            } else if (/@$/.test(matches[1]) && !/^mailto:/.test(matches[1])) {
                matches[1] = `mailto:${matches[1]}`;
            }
            bookmark = editor.selection.getBookmark();
            editor.selection.setRng(rng);
            editor.execCommand("createlink", false, matches[1] + matches[2]);
            if (defaultLinkTarget !== false) {
                editor.dom.setAttrib(editor.selection.getNode(), "target", defaultLinkTarget);
            }
            editor.selection.moveToBookmark(bookmark);
            editor.nodeChanged();
        } else if (phoneMatches) {
            const [prettyPrintedNumber, callableNumber] = convertToInternational(phoneMatches[0]);
            bookmark = editor.selection.getBookmark();
            editor.selection.setRng(rng);
            editor.execCommand("createlink", false, callableNumber);
            if (defaultLinkTarget !== false) {
                editor.dom.setAttrib(editor.selection.getNode(), "target", defaultLinkTarget);
                editor.selection.setContent(prettyPrintedNumber);
            }
            editor.selection.moveToBookmark(bookmark);
            editor.nodeChanged();
        }
    };
    const handleEclipse = (editor) => {
        parseCurrentLine(editor, -1, "(");
    };
    const handleSpacebar = (editor) => {
        parseCurrentLine(editor, 0, "");
    };
    const handleEnter = (editor) => {
        parseCurrentLine(editor, -1, "");
    };
    const setup = (editor) => {
        const KEY_CODE_ENTER = 13;
        const KEY_CODE_SPACE = 32;
        const KEY_CODE_SELECT = 41;

        let autoUrlDetectState;
        /* eslint-disable-next-line consistent-return */
        editor.on("keydown", (e) => {
            if (e.keyCode === KEY_CODE_ENTER) {
                return handleEnter(editor);
            }
        });
        if (global$1.browser.isIE()) {
            editor.on("focus", () => {
                if (!autoUrlDetectState) {
                    autoUrlDetectState = true;
                    try {
                        editor.execCommand("AutoUrlDetect", false, true);
                    } catch (ex) {}
                }
            });
            return "";
        }
        /* eslint-disable-next-line consistent-return */
        editor.on("keypress", (e) => {
            if (e.keyCode === KEY_CODE_SELECT) {
                return handleEclipse(editor);
            }
        });
        /* eslint-disable-next-line consistent-return */
        editor.on("keyup", (e) => {
            if (e.keyCode === KEY_CODE_SPACE) {
                return handleSpacebar(editor);
            }
        });
        return "";
    };

    const Plugin = () => {
        global.add("autolink_tel", (editor) => {
            setup(editor);
        });
    };

    Plugin();
})();