<template>
    <div contenteditable @input="onchange" ref="text">
    </div>
</template>

<script>
export default {
    name: 'FormatedText',
    props: {
        value: {
            type: String,
            required: true
        },
        format: {
            type: Function,
            default: null
        }
    },
    data() {
        return {
            selection: {start: 0, end: 0, direction: 'forward', type: 'Caret'}
        };
    },
    emits: ['input'],
    methods: {
        rawhtml(value) {
            if (typeof this.format === 'function') {
                return this.format(value.replace(/ /g, '&nbsp;'));
            } else {
                return value;
            }
        },
        onchange(event) {
            const div = this.$refs.text;
            const sel = window.getSelection();
            if (sel.rangeCount > 0) {
                this.selection.start = this.calculateOffset(div, sel.anchorNode, sel.anchorOffset);
                this.selection.end = this.calculateOffset(div, sel.focusNode, sel.focusOffset);
                this.selection.direction = sel.direction;
                this.selection.type = sel.type;
            }
            this.$emit('input', event.target.innerText.replace(/&nbsp;/g, ' ').replace(/\xA0/g, ' '));
        },
        calculateOffset(container, node, offset) {
            let position = 0;
            let found = false;
            const walk = (elem) => {
                if (elem === node) {
                    found = true;
                    return;
                }
                if (elem.nodeType === 3) {
                    position += elem.length;
                } else {
                    for (let i = 0; i < elem.childNodes.length; i++) {
                        walk(elem.childNodes[i]);
                        if (found) {
                            return;
                        }
                    }
                }
            };
            walk(container);
            return position + offset;
        },
        findNode(container, offset) {
            let position = 0;
            let found = false;
            let node = null;
            const walk = (elem) => {
                if (position + elem.length >= offset) {
                    found = true;
                    node = elem;
                    return;
                }
                if (elem.nodeType === 3) {
                    position += elem.length;
                } else {
                    for (let i = 0; i < elem.childNodes.length; i++) {
                        walk(elem.childNodes[i]);
                        if (found) {
                            return;
                        }
                    }
                }
            };
            walk(container);
            return [node, offset - position]
        },
    },
    watch: {
        value() {
            if (this.selection) {
                const div = this.$refs.text;
                div.innerHTML = this.rawhtml(this.value);

                const range = document.createRange();
                const sel = window.getSelection();
                range.setStart(...this.findNode(div, this.selection.start));
                range.setEnd(...this.findNode(div, this.selection.end));

                sel.removeAllRanges();
                sel.addRange(range);
            }
        }
    },
    mounted() {
        const div = this.$refs.text;
        div.innerHTML = this.rawhtml(this.value);
    }
};
</script>