"use strict";
/* eslint-disable */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AutoComplete = void 0;
// @ts-ignore
const objectSpread2_1 = __importDefault(require("@babel/runtime/helpers/objectSpread2"));
// @ts-ignore
const toConsumableArray_1 = __importDefault(require("@babel/runtime/helpers/toConsumableArray"));
// @ts-ignore
const createForOfIteratorHelper_1 = __importDefault(require("@babel/runtime/helpers/createForOfIteratorHelper"));
var select$1 = function select(element) {
    const el = typeof element === 'string' ? document.querySelector(element) : element();
    if (el == null)
        throw "Element can't be create";
    return el;
};
var create = function create(tag, options) {
    var _a;
    var el = typeof tag === 'string' ? document.createElement(tag) : tag;
    for (var key in options) {
        var val = options[key];
        if (key === 'inside') {
            val.append(el);
        }
        else if (key === 'dest') {
            (_a = select$1(val[0])) === null || _a === void 0 ? void 0 : _a.insertAdjacentElement(val[1], el);
        }
        else if (key === 'around') {
            var ref = val;
            ref.parentNode.insertBefore(el, ref);
            el.append(ref);
            if (ref.getAttribute('autofocus') != null)
                ref.focus();
        }
        else if (key in el) {
            // @ts-ignore
            el[key] = val;
        }
        else {
            el.setAttribute(key, val);
        }
    }
    return el;
};
var getQuery = function getQuery(field) {
    return field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement
        ? field.value
        : field.innerHTML;
};
var format = function format(value, diacritics) {
    value = String(value).toLowerCase();
    return diacritics
        ? value
            .normalize('NFD')
            .replace(/[\u0300-\u036f]/g, '')
            .normalize('NFC')
        : value;
};
var debounce = function debounce(callback, duration) {
    var timer;
    return function () {
        clearTimeout(timer);
        timer = setTimeout(function () {
            return callback();
        }, duration);
    };
};
var checkTrigger = function checkTrigger(query, condition, threshold) {
    return condition ? condition(query) : query.length >= threshold;
};
var mark = function mark(value, cls) {
    return create('mark', (0, objectSpread2_1.default)({
        innerHTML: value
    }, typeof cls === 'string' && {
        class: cls
    })).outerHTML;
};
var Expand = 'aria-expanded';
var Active = 'aria-activedescendant';
var Selected = 'aria-selected';
let instances = 0;
const defaultValue = {
    name: 'autoComplete',
    selector: '#autoComplete',
    wrapper: true,
    data: {
        src: null,
        keys: null,
        cache: false,
        filter: null
    },
    threshold: 1,
    debounce: 0,
    searchEngine: 'strict',
    diacritics: false,
    resultsList: {
        tag: 'ul',
        position: 'afterend',
        maxResults: 5,
        tabSelect: false,
        noResults: false
    },
    resultItem: {
        tag: 'li',
        highlight: false
    },
    submit: false
};
class AutoComplete {
    constructor(props) {
        this.isOpen = false;
        this.id = instances = instances + 1;
        this.props = Object.assign(Object.assign({}, defaultValue), props);
        if (props.data)
            this.props.data = Object.assign(Object.assign({}, defaultValue.data), props.data);
        if (props.resultsList)
            this.props.resultsList = Object.assign(Object.assign({}, defaultValue.resultsList), props.resultsList);
        if (props.resultItem)
            this.props.resultItem = Object.assign(Object.assign({}, defaultValue.resultItem), props.resultItem);
        this.configure();
        this.init();
    }
    init() {
        return new Promise(($return, $error) => {
            var _a;
            var placeHolder, resultsList, parentAttrs;
            placeHolder = this.props.placeHolder;
            resultsList = this.props.resultsList;
            parentAttrs = {
                role: 'combobox',
                'aria-owns': resultsList.id,
                'aria-haspopup': true,
                'aria-expanded': false
            };
            create(this.input, (0, objectSpread2_1.default)((0, objectSpread2_1.default)({
                'aria-controls': resultsList.id,
                'aria-autocomplete': 'both'
            }, placeHolder && {
                placeholder: placeHolder
            }), !this.props.wrapper && (0, objectSpread2_1.default)({}, parentAttrs)));
            if (this.props.wrapper)
                this.wrapper = create('div', (0, objectSpread2_1.default)({
                    around: this.input,
                    class: this.props.name + '_wrapper'
                }, parentAttrs));
            if (resultsList)
                this.list = create(resultsList.tag, (0, objectSpread2_1.default)({
                    dest: [resultsList.destination, resultsList.position],
                    id: resultsList.id,
                    role: 'listbox',
                    hidden: 'hidden'
                }, resultsList['class'] && {
                    class: resultsList['class']
                }));
            this.addEvents();
            if ((_a = this.props.data) === null || _a === void 0 ? void 0 : _a.cache) {
                return this.getData().then(() => {
                    try {
                        this.eventEmitter('init');
                        $return();
                    }
                    catch ($boundEx) {
                        $error($boundEx);
                    }
                }, $error);
            }
            this.eventEmitter('init');
            $return();
        });
    }
    unInit() {
        if (this.props.wrapper) {
            if (this.wrapper) {
                var parentNode = this.wrapper.parentNode;
                if (parentNode) {
                    parentNode.insertBefore(this.input, this.wrapper);
                    parentNode.removeChild(this.wrapper);
                }
            }
        }
        this.removeEvents();
    }
    configure() {
        var name = this.props.name, resultsList = this.props.resultsList, resultItem = this.props.resultItem;
        this.props.selector = this.props.selector || '#' + name;
        resultsList.destination = resultsList.destination || this.props.selector;
        resultsList.id = resultsList.id || name + '_list_' + this.id;
        resultItem.id = resultItem.id || name + '_result';
        this.input = select$1(this.props.selector);
    }
    eventEmitter(name) {
        this.input.dispatchEvent(new CustomEvent(name, {
            bubbles: true,
            detail: this.feedback,
            cancelable: true
        }));
    }
    search(query, record, options) {
        var _ref = options || {}, mode = _ref.mode, diacritics = _ref.diacritics, highlight = _ref.highlight;
        var nRecord = format(record, diacritics);
        record = String(record);
        query = format(query, diacritics);
        if (mode === 'loose') {
            query = query.replace(/ /g, '');
            var qLength = query.length;
            var cursor = 0;
            var match = Array.from(record)
                .map((character, index) => {
                if (cursor < qLength && nRecord[index] === query[cursor]) {
                    character = highlight ? mark(character, highlight) : character;
                    cursor++;
                }
                return character;
            })
                .join('');
            if (cursor === qLength)
                return match;
        }
        else {
            var _match = nRecord.indexOf(query);
            if (~_match) {
                query = record.substring(_match, _match + query.length);
                _match = highlight ? record.replace(query, mark(query, highlight)) : record;
                return _match;
            }
        }
    }
    getData(query) {
        return new Promise((resolve, reject) => {
            var data = this.props.data;
            if ((data === null || data === void 0 ? void 0 : data.cache) && data.store)
                return resolve();
            return new Promise((resolve1, reject1) => {
                if (data && typeof data.src === 'function') {
                    return new Promise((resolve2, reject2) => {
                        if (typeof data.src === 'function' &&
                            data.src.constructor.name === 'AsyncFunction') {
                            // @ts-ignore
                            return data.src(query).then(resolve2, reject2);
                        }
                        return resolve2(typeof data.src == 'function' && data.src(query));
                    }).then(resolve1, reject1);
                }
                return resolve1(data === null || data === void 0 ? void 0 : data.src);
            }).then(($await_7) => {
                try {
                    this.feedback = data.store = $await_7;
                    this.eventEmitter('response');
                    return resolve();
                }
                catch ($boundEx) {
                    return reject($boundEx);
                }
            }, reject);
        });
    }
    findMatches(query) {
        var _a, _b;
        var data = this.props.data, searchEngine = this.props.searchEngine;
        var matches = [];
        (_a = data === null || data === void 0 ? void 0 : data.store) === null || _a === void 0 ? void 0 : _a.forEach((value) => {
            var find = (key) => {
                var _a;
                var record = key ? value[key] : value;
                var match = typeof searchEngine === 'function'
                    ? searchEngine(query, record)
                    : this.search(query, record, {
                        mode: searchEngine,
                        diacritics: this.props.diacritics,
                        highlight: (_a = this.props.resultItem) === null || _a === void 0 ? void 0 : _a.highlight
                    });
                if (!match)
                    return;
                var result = {
                    match: match,
                    value: value
                };
                if (key)
                    result.key = key;
                matches.push(result);
            };
            if (data === null || data === void 0 ? void 0 : data.keys) {
                var _iterator = (0, createForOfIteratorHelper_1.default)(data.keys), _step;
                try {
                    for (_iterator.s(); !(_step = _iterator.n()).done;) {
                        var key = _step.value;
                        find(key);
                    }
                }
                catch (err) {
                    _iterator.e(err);
                }
                finally {
                    _iterator.f();
                }
            }
            else {
                find();
            }
        });
        if (data === null || data === void 0 ? void 0 : data.filter)
            matches = data.filter(matches);
        var results = matches.slice(0, (_b = this.props.resultsList) === null || _b === void 0 ? void 0 : _b.maxResults);
        this.feedback = {
            query: query,
            matches: matches,
            results: results
        };
        this.eventEmitter('results');
    }
    setFeedback(index) {
        this.feedback.selection = (0, objectSpread2_1.default)({
            index: index
        }, this.feedback.results[index]);
    }
    render() {
        var resultsList = this.props.resultsList, list = this.list, resultItem = this.props.resultItem, feedback = this.feedback;
        var matches = feedback.matches, results = feedback.results;
        this.cursor = -1;
        list.innerHTML = '';
        if (matches.length || resultsList.noResults) {
            var fragment = new DocumentFragment();
            results.forEach((result, index) => {
                if (resultItem && resultItem.id && resultItem.tag) {
                    var element = create(resultItem.tag, (0, objectSpread2_1.default)({
                        id: ''.concat(resultItem.id, '_').concat(index),
                        role: 'option',
                        innerHTML: result.match,
                        inside: fragment
                    }, resultItem.class && {
                        class: resultItem.class
                    }));
                    if (resultItem.element)
                        resultItem.element(element, result);
                }
            });
            list.append(fragment);
            if (resultsList.element)
                resultsList.element(list, feedback);
            this.open();
        }
        else {
            this.close();
        }
    }
    open() {
        if (this.isOpen)
            return;
        (this.wrapper || this.input).setAttribute(Expand, 'true');
        this.list.removeAttribute('hidden');
        this.isOpen = true;
        this.eventEmitter('open');
    }
    close() {
        if (!this.isOpen)
            return;
        (this.wrapper || this.input).setAttribute(Expand, 'false');
        this.input.setAttribute(Active, '');
        this.list.setAttribute('hidden', '');
        this.isOpen = false;
        this.eventEmitter('close');
    }
    goTo(index) {
        var ctx = this;
        var resultItem = this.props.resultItem;
        var results = ctx.list.getElementsByTagName(resultItem === null || resultItem === void 0 ? void 0 : resultItem.tag);
        var cls = (resultItem === null || resultItem === void 0 ? void 0 : resultItem.selected) ? resultItem === null || resultItem === void 0 ? void 0 : resultItem.selected.split(' ') : false;
        if (ctx.isOpen && results.length) {
            var _results$index$classL;
            var state = ctx.cursor;
            if (index >= results.length)
                index = 0;
            if (index < 0)
                index = results.length - 1;
            ctx.cursor = index;
            if (state > -1) {
                var _results$state$classL;
                results[state].removeAttribute(Selected);
                if (cls)
                    (_results$state$classL = results[state].classList).remove.apply(_results$state$classL, (0, toConsumableArray_1.default)(cls));
            }
            results[index].setAttribute(Selected, true);
            if (cls)
                (_results$index$classL = results[index].classList).add.apply(_results$index$classL, (0, toConsumableArray_1.default)(cls));
            ctx.input.setAttribute(Active, results[ctx.cursor].id);
            ctx.list.scrollTop =
                results[index].offsetTop - ctx.list.clientHeight + results[index].clientHeight + 5;
            ctx.feedback.cursor = ctx.cursor;
            this.setFeedback(index);
            this.eventEmitter('navigate');
        }
    }
    next() {
        this.goTo(this.cursor + 1);
    }
    previous() {
        this.goTo(this.cursor - 1);
    }
    select(event, index) {
        index = index >= 0 ? index : this.cursor;
        if (index < 0)
            return;
        this.feedback.event = event;
        this.setFeedback(index);
        this.eventEmitter('selection');
        this.close();
    }
    click(event) {
        var _a, _b;
        var itemTag = (_b = (_a = this.props.resultItem) === null || _a === void 0 ? void 0 : _a.tag) === null || _b === void 0 ? void 0 : _b.toUpperCase();
        var items = Array.from(this.list.querySelectorAll(itemTag));
        var item = event.target.closest(itemTag);
        if (item && item.nodeName === itemTag) {
            this.select(event, items.indexOf(item));
        }
    }
    navigate(event) {
        switch (event.keyCode) {
            case 40:
            case 38:
                event.preventDefault();
                event.keyCode === 40 ? this.next() : this.previous();
                break;
            case 13:
                if (!this.props.submit)
                    event.preventDefault();
                if (this.cursor >= 0)
                    this.select(event);
                break;
            case 9:
                if (this.props.resultsList.tabSelect && this.cursor >= 0)
                    this.select(event);
                break;
            case 27:
                this.input.value = '';
                this.close();
                break;
        }
    }
    start(q) {
        return new Promise((resolve, reject) => {
            let queryVal = q || getQuery(this.input);
            queryVal = this.props.query ? this.props.query(queryVal) : queryVal;
            let condition = checkTrigger(queryVal, this.props.trigger, this.props.threshold);
            if (condition) {
                return this.getData(queryVal).then(() => {
                    try {
                        if (this.feedback instanceof Error)
                            return resolve();
                        this.findMatches(queryVal);
                        if (this.props.resultsList)
                            this.render();
                        return resolve();
                    }
                    catch ($boundEx) {
                        return reject($boundEx);
                    }
                }, reject);
            }
            else {
                this.close();
                return resolve();
            }
        });
    }
    eventsManager(events, callback) {
        for (var element in events) {
            for (var event in events[element]) {
                callback(element, event);
            }
        }
    }
    addEvents() {
        var events = this.props.events;
        var run = debounce(() => {
            return this.start();
        }, this.props.debounce);
        var publicEvents = (this.props.events = (0, objectSpread2_1.default)({
            input: (0, objectSpread2_1.default)({}, events && events.input)
        }, this.props.resultsList && {
            list: events ? (0, objectSpread2_1.default)({}, events.list) : {}
        }));
        var privateEvents = {
            input: {
                input: () => {
                    run();
                },
                keydown: (event) => {
                    this.navigate(event);
                },
                blur: () => {
                    this.close();
                }
            },
            list: {
                mousedown: (event) => {
                    event.preventDefault();
                },
                click: (event) => {
                    this.click(event);
                }
            }
        };
        this.eventsManager(privateEvents, (element, event) => {
            if (!this.props.resultsList && event !== 'input')
                return;
            if (publicEvents[element][event])
                return;
            publicEvents[element][event] = privateEvents[element][event];
        });
        this.eventsManager(publicEvents, (element, event) => {
            switch (element) {
                case 'input':
                    this.input.addEventListener(event, publicEvents[element][event]);
                    break;
                case 'list':
                    this.list.addEventListener(event, publicEvents[element][event]);
                    break;
                default:
            }
            //this[element].addEventListener(event, publicEvents[element][event]);
        });
    }
    removeEvents() {
        this.eventsManager(this.props.events, (element, event) => {
            switch (element) {
                case 'input':
                    if (this.props.events)
                        this.input.removeEventListener(event, this.props.events[element][event]);
                    break;
                case 'list':
                    if (this.props.events)
                        this.list.removeEventListener(event, this.props.events[element][event]);
                    break;
                default:
            }
            //this[element].removeEventListener(event, this.events[element][event]);
        });
    }
}
exports.AutoComplete = AutoComplete;
