/*!
 * Custom elements for Nanopore-Digital Web applications
 */
import { EvEmitter } from "./utils/event-emitter";
import { getSize } from "./utils/get-size";
import { utils } from "./utils/utils";
import Cell from "./cell";
import Slide from "./slide";
import animatePrototype from "./animate";
// vars
const getComputedStyle = window.getComputedStyle;
const console = window.console;
function moveElements(elems, toElem) {
    elems = utils.makeArray(elems);
    while (elems.length) {
        toElem.appendChild(elems.shift());
    }
}
// -------------------------- Flickity -------------------------- //
// globally unique identifiers
let GUID = 0;
// internal store of all Flickity intances
const instances = {};
export default function Flickity(element, options) {
    const queryElement = utils.getQueryElement(element);
    if (!queryElement) {
        if (console) {
            console.error('Bad element for Flickity: ' + (queryElement || element));
        }
        return;
    }
    this.element = queryElement;
    // do not initialize twice on same element
    if (this.element.flickityGUID) {
        const instance = instances[this.element.flickityGUID];
        if (!instances[this.element.flickityGUID])
            return;
        instance.option(options);
        return instance;
    }
    // options
    this.options = utils.extend({}, this.constructor.defaults);
    this.option(options);
    // kick things off
    this._create();
}
Flickity.defaults = {
    accessibility: true,
    // adaptiveHeight: false,
    cellAlign: 'center',
    // cellSelector: undefined,
    // contain: false,
    freeScrollFriction: 0.075, // friction when free-scrolling
    friction: 0.28, // friction when selecting
    namespaceJQueryEvents: true,
    // initialIndex: 0,
    percentPosition: true,
    resize: true,
    selectedAttraction: 0.025,
    setGallerySize: true,
    // watchCSS: false,
    // wrapAround: false
};
// hash of methods triggered on _create()
Flickity.createMethods = [];
const proto = Flickity.prototype;
// inherit EventEmitter
utils.extend(proto, EvEmitter.prototype);
proto._create = function () {
    // add id for Flickity.data
    const id = (this.guid = ++GUID);
    this.element.flickityGUID = id; // expando
    instances[id] = this; // associate via id
    // initial properties
    this.selectedIndex = 0;
    // how many frames slider has been in same position
    this.restingFrames = 0;
    // initial physics properties
    this.x = 0;
    this.velocity = 0;
    this.originSide = this.options.rightToLeft ? 'right' : 'left';
    // create viewport & slider
    this.viewport = document.createElement('div');
    this.viewport.className = 'flickity-viewport';
    this.viewport.setAttribute('part', 'slide-viewport');
    this._createSlider();
    if (this.options.resize || this.options.watchCSS) {
        window.addEventListener('resize', this);
    }
    // add listeners from on option
    for (const eventName in this.options.on) {
        const listener = this.options.on[eventName];
        this.on(eventName, listener);
    }
    Flickity.createMethods.forEach(function (method) {
        this[method]();
    }, this);
    if (this.options.watchCSS) {
        this.watchCSS();
    }
    else {
        this.activate();
    }
};
/**
 * set options
 * @param {Object} opts
 */
proto.option = function (opts) {
    utils.extend(this.options, opts);
};
proto.activate = function () {
    if (this.isActive) {
        return;
    }
    this.isActive = true;
    this.element.classList.add('flickity-enabled');
    if (this.options.rightToLeft) {
        this.element.classList.add('flickity-rtl');
    }
    this.getSize();
    // move initial cell elements so they can be loaded as cells
    const cellElems = this.element.children; //this._filterFindCellElements( this.element.children );
    moveElements(cellElems, this.slider);
    this.viewport.appendChild(this.slider);
    this.element.appendChild(this.viewport);
    // get cells from children
    this.reloadCells();
    if (this.options.accessibility) {
        // allow element to focusable
        this.element.tabIndex = 0;
        // listen for key presses
        this.element.addEventListener('keydown', this);
    }
    this.emitEvent('activate');
    this.selectInitialIndex();
    // flag for initial activation, for using initialIndex
    this.isInitActivated = true;
    // ready event. #493
    this.dispatchEvent('ready');
};
// slider positions the cells
proto._createSlider = function () {
    // slider element does all the positioning
    const slider = document.createElement('div');
    slider.className = 'flickity-slider';
    slider.style[this.originSide] = 0;
    this.slider = slider;
};
proto._filterFindCellElements = function (elems) {
    return utils.filterFindElements(elems, this.options.cellSelector);
};
// goes through all children
proto.reloadCells = function () {
    let children = this.slider.children;
    if (this.slider.getRootNode().host) {
        children = [];
        for (const ele of this.slider.getRootNode().host.children) {
            if ('assignedElements' in ele) {
                children = [...children, ...ele.assignedElements()];
            }
            else {
                children.push(ele);
            }
        }
    }
    // collection of item elements
    this.cells = this._makeCells(children);
    this.positionCells();
    this._getWrapShiftCells();
    this.setGallerySize();
};
/**
 * turn elements into Flickity.Cells
 * @param {Array or NodeList or HTMLElement} elems
 * @returns {Array} items - collection of new Flickity Cells
 */
proto._makeCells = function (elems) {
    const cellElems = this._filterFindCellElements(elems);
    // create new Flickity for collection
    const cells = cellElems.map(function (cellElem) {
        return new Cell(cellElem, this);
    }, this);
    return cells;
};
proto.getLastCell = function () {
    return this.cells[this.cells.length - 1];
};
proto.getLastSlide = function () {
    return this.slides[this.slides.length - 1];
};
// positions all cells
proto.positionCells = function () {
    // size all cells
    this._sizeCells(this.cells);
    // position all cells
    this._positionCells(0);
};
/**
 * position certain cells
 * @param {Integer} index - which cell to start with
 */
proto._positionCells = function (index) {
    index = index || 0;
    // also measure maxCellHeight
    // start 0 if positioning all cells
    this.maxCellHeight = index ? this.maxCellHeight || 0 : 0;
    let cellX = 0;
    // get cellX
    if (index > 0) {
        const startCell = this.cells[index - 1];
        cellX = startCell.x + startCell.size.outerWidth;
    }
    const len = this.cells.length;
    for (let i = index; i < len; i++) {
        const cell = this.cells[i];
        cell.setPosition(cellX);
        cellX += cell.size.outerWidth;
        this.maxCellHeight = Math.max(cell.size.outerHeight, this.maxCellHeight);
    }
    // keep track of cellX for wrap-around
    this.slideableWidth = cellX;
    // slides
    this.updateSlides();
    // contain slides target
    this._containSlides();
    // update slidesWidth
    this.slidesWidth = len
        ? this.getLastSlide().target - this.slides[0].target
        : 0;
};
/**
 * cell.getSize() on multiple cells
 * @param {Array} cells
 */
proto._sizeCells = function (cells) {
    cells.forEach(function (cell) {
        cell.getSize();
    });
};
// --------------------------  -------------------------- //
proto.updateSlides = function () {
    this.slides = [];
    if (!this.cells.length) {
        return;
    }
    let slide = new Slide(this);
    this.slides.push(slide);
    const isOriginLeft = this.originSide == 'left';
    const nextMargin = isOriginLeft ? 'marginRight' : 'marginLeft';
    const canCellFit = this._getCanCellFit();
    this.cells.forEach(function (cell, i) {
        // just add cell if first cell in slide
        if (!slide.cells.length) {
            slide.addCell(cell);
            return;
        }
        const slideWidth = slide.outerWidth -
            slide.firstMargin +
            (cell.size.outerWidth - cell.size[nextMargin]);
        if (canCellFit.call(this, i, slideWidth)) {
            slide.addCell(cell);
        }
        else {
            // doesn't fit, new slide
            slide.updateTarget();
            slide = new Slide(this);
            this.slides.push(slide);
            slide.addCell(cell);
        }
    }, this);
    // last slide
    slide.updateTarget();
    // update .selectedSlide
    this.updateSelectedSlide();
};
proto._getCanCellFit = function () {
    const groupCells = this.options.groupCells;
    if (!groupCells) {
        return function () {
            return false;
        };
    }
    else if (typeof groupCells == 'number') {
        // group by number. 3 -> [0,1,2], [3,4,5], ...
        // @ts-ignore
        const number = parseInt(groupCells, 10);
        return function (i) {
            return i % number !== 0;
        };
    }
    // default, group by width of slide
    // parse '75%
    const percentMatch = typeof groupCells == 'string' && groupCells.match(/^(\d+)%$/);
    const percent = percentMatch ? parseInt(percentMatch[1], 10) / 100 : 1;
    return function (_i, slideWidth) {
        return slideWidth <= (this.size.innerWidth + 1) * percent;
    };
};
// alias _init for jQuery plugin .flickity()
proto._init = proto.reposition = function () {
    this.positionCells();
    this.positionSliderAtSelected();
};
proto.getSize = function () {
    this.size = getSize(this.element);
    this.setCellAlign();
    this.cursorPosition = this.size.innerWidth * this.cellAlign;
};
const cellAlignShorthands = {
    // cell align, then based on origin side
    center: {
        left: 0.5,
        right: 0.5,
    },
    left: {
        left: 0,
        right: 1,
    },
    right: {
        right: 0,
        left: 1,
    },
};
proto.setCellAlign = function () {
    const shorthand = cellAlignShorthands[this.options.cellAlign];
    this.cellAlign = shorthand
        ? shorthand[this.originSide]
        : this.options.cellAlign;
};
proto.setGallerySize = function () {
    if (this.options.setGallerySize) {
        const height = this.options.adaptiveHeight && this.selectedSlide
            ? this.selectedSlide.height
            : this.maxCellHeight;
        this.viewport.style.height = height + 'px';
    }
};
proto._getWrapShiftCells = function () {
    // only for wrap-around
    if (!this.options.wrapAround) {
        return;
    }
    // unshift previous cells
    this._unshiftCells(this.beforeShiftCells);
    this._unshiftCells(this.afterShiftCells);
    // get before cells
    // initial gap
    let gapX = this.cursorPosition;
    const cellIndex = this.cells.length - 1;
    this.beforeShiftCells = this._getGapCells(gapX, cellIndex, -1);
    // get after cells
    // ending gap between last cell and end of gallery viewport
    gapX = this.size.innerWidth - this.cursorPosition;
    // start cloning at first cell, working forwards
    this.afterShiftCells = this._getGapCells(gapX, 0, 1);
};
proto._getGapCells = function (gapX, cellIndex, increment) {
    // keep adding cells until the cover the initial gap
    const cells = [];
    while (gapX > 0) {
        const cell = this.cells[cellIndex];
        if (!cell) {
            break;
        }
        cells.push(cell);
        cellIndex += increment;
        gapX -= cell.size.outerWidth;
    }
    return cells;
};
// ----- contain ----- //
// contain cell targets so no excess sliding
proto._containSlides = function () {
    if (!this.options.contain || this.options.wrapAround || !this.cells.length) {
        return;
    }
    const isRightToLeft = this.options.rightToLeft;
    const beginMargin = isRightToLeft ? 'marginRight' : 'marginLeft';
    const endMargin = isRightToLeft ? 'marginLeft' : 'marginRight';
    const contentWidth = this.slideableWidth - this.getLastCell().size[endMargin];
    // content is less than gallery size
    const isContentSmaller = contentWidth < this.size.innerWidth;
    // bounds
    const beginBound = this.cursorPosition + this.cells[0].size[beginMargin];
    const endBound = contentWidth - this.size.innerWidth * (1 - this.cellAlign);
    // contain each cell target
    this.slides.forEach(function (slide) {
        if (isContentSmaller) {
            // all cells fit inside gallery
            slide.target = contentWidth * this.cellAlign;
        }
        else {
            // contain to bounds
            slide.target = Math.max(slide.target, beginBound);
            slide.target = Math.min(slide.target, endBound);
        }
    }, this);
};
// -----  ----- //
/**
 * emits events via eventEmitter and jQuery events
 * @param {String} type - name of event
 * @param {Event} event - original event
 * @param {Array} args - extra arguments
 */
proto.dispatchEvent = function (type, event, args) {
    const emitArgs = event ? [event].concat(args) : args;
    this.emitEvent(type, emitArgs);
};
// -------------------------- select -------------------------- //
/**
 * @param {Integer} index - index of the slide
 * @param {Boolean} isWrap - will wrap-around to last/first if at the end
 * @param {Boolean} isInstant - will immediately set position at selected cell
 */
proto.select = function (index, isWrap, isInstant) {
    if (!this.isActive) {
        return;
    }
    index = parseInt(index, 10);
    this._wrapSelect(index);
    if (this.options.wrapAround || isWrap) {
        index = utils.modulo(index, this.slides.length);
    }
    // bail if invalid index
    if (!this.slides[index]) {
        return;
    }
    const prevIndex = this.selectedIndex;
    this.selectedIndex = index;
    this.updateSelectedSlide();
    if (isInstant) {
        this.positionSliderAtSelected();
    }
    else {
        this.startAnimation();
    }
    if (this.options.adaptiveHeight) {
        this.setGallerySize();
    }
    // events
    this.dispatchEvent('select', null, [index]);
    // change event if new index
    if (index != prevIndex) {
        this.dispatchEvent('change', null, [index]);
    }
    // old v1 event name, remove in v3
    this.dispatchEvent('cellSelect');
};
// wraps position for wrapAround, to move to closest slide. #113
proto._wrapSelect = function (index) {
    const len = this.slides.length;
    const isWrapping = this.options.wrapAround && len > 1;
    if (!isWrapping) {
        return index;
    }
    const wrapIndex = utils.modulo(index, len);
    // go to shortest
    const delta = Math.abs(wrapIndex - this.selectedIndex);
    const backWrapDelta = Math.abs(wrapIndex + len - this.selectedIndex);
    const forewardWrapDelta = Math.abs(wrapIndex - len - this.selectedIndex);
    if (!this.isDragSelect && backWrapDelta < delta) {
        index += len;
    }
    else if (!this.isDragSelect && forewardWrapDelta < delta) {
        index -= len;
    }
    // wrap position so slider is within normal area
    if (index < 0) {
        this.x -= this.slideableWidth;
    }
    else if (index >= len) {
        this.x += this.slideableWidth;
    }
};
proto.previous = function (isWrap, isInstant) {
    this.select(this.selectedIndex - 1, isWrap, isInstant);
};
proto.next = function (isWrap, isInstant) {
    this.select(this.selectedIndex + 1, isWrap, isInstant);
};
proto.updateSelectedSlide = function () {
    const slide = this.slides[this.selectedIndex];
    // selectedIndex could be outside of slides, if triggered before resize()
    if (!slide) {
        return;
    }
    // unselect previous selected slide
    this.unselectSelectedSlide();
    // update new selected slide
    this.selectedSlide = slide;
    slide.select();
    this.selectedCells = slide.cells;
    this.selectedElements = slide.getCellElements();
    // HACK: selectedCell & selectedElement is first cell in slide, backwards compatibility
    // Remove in v3?
    this.selectedCell = slide.cells[0];
    this.selectedElement = this.selectedElements[0];
};
proto.unselectSelectedSlide = function () {
    if (this.selectedSlide) {
        this.selectedSlide.unselect();
    }
};
proto.selectInitialIndex = function () {
    const initialIndex = this.options.initialIndex;
    // already activated, select previous selectedIndex
    if (this.isInitActivated) {
        this.select(this.selectedIndex, false, true);
        return;
    }
    // select with selector string
    if (initialIndex && typeof initialIndex == 'string') {
        const cell = this.queryCell(initialIndex);
        if (cell) {
            this.selectCell(initialIndex, false, true);
            return;
        }
    }
    let index = 0;
    // select with number
    if (initialIndex && this.slides[initialIndex]) {
        index = initialIndex;
    }
    // select instantly
    this.select(index, false, true);
};
/**
 * select slide from number or cell element
 * @param {Element or Number} elem
 * @param { Boolean } isWrap
 * @param { Boolean } isInstant
 */
proto.selectCell = function (elem, isWrap, isInstant) {
    // get cell
    const cell = this.queryCell(elem);
    if (!cell) {
        return;
    }
    const index = this.getCellSlideIndex(cell);
    this.select(index, isWrap, isInstant);
};
proto.getCellSlideIndex = function (cell) {
    // get index of slides that has cell
    for (let i = 0; i < this.slides.length; i++) {
        const slide = this.slides[i];
        const index = slide.cells.indexOf(cell);
        if (index != -1) {
            return i;
        }
    }
};
// -------------------------- get cells -------------------------- //
/**
 * get Flickity.Cell, given an Element
 * @param {Element} elem
 * @returns found flickity cell
 */
proto.getCell = function (elem) {
    // loop through cells to get the one that matches
    for (let i = 0; i < this.cells.length; i++) {
        const cell = this.cells[i];
        if (cell.element == elem) {
            return cell;
        }
    }
};
/**
 * get collection of Flickity.Cells, given Elements
 * @param {Element, Array, NodeList} elems
 * @returns {Array} cells - Flickity.Cells
 */
proto.getCells = function (elems) {
    elems = utils.makeArray(elems);
    const cells = [];
    elems.forEach(function (elem) {
        const cell = this.getCell(elem);
        if (cell) {
            cells.push(cell);
        }
    }, this);
    return cells;
};
/**
 * get cell elements
 * @returns {Array} cellElems
 */
proto.getCellElements = function () {
    return this.cells.map(function (cell) {
        return cell.element;
    });
};
/**
 * get parent cell from an element
 * @param {Element} elem
 * @returns {Flickit.Cell} cell
 */
proto.getParentCell = function (elem) {
    // first check if elem is cell
    const cell = this.getCell(elem);
    if (cell) {
        return cell;
    }
    // try to get parent cell elem
    elem = utils.getParent(elem, '.flickity-slider > *');
    return this.getCell(elem);
};
/**
 * get cells adjacent to a slide
 * @param {Integer} adjCount - number of adjacent slides
 * @param {Integer} index - index of slide to start
 * @returns {Array} cells - array of Flickity.Cells
 */
proto.getAdjacentCellElements = function (adjCount, index) {
    if (!adjCount) {
        return this.selectedSlide.getCellElements();
    }
    index = index === undefined ? this.selectedIndex : index;
    const len = this.slides.length;
    if (1 + adjCount * 2 >= len) {
        return this.getCellElements();
    }
    let cellElems = [];
    for (let i = index - adjCount; i <= index + adjCount; i++) {
        const slideIndex = this.options.wrapAround ? utils.modulo(i, len) : i;
        const slide = this.slides[slideIndex];
        if (slide) {
            cellElems = cellElems.concat(slide.getCellElements());
        }
    }
    return cellElems;
};
/**
 * select slide from number or cell element
 * @param {Element, Selector String, or Number} selector
 * @returns {Flickity.Cell} found flickity cell
 */
proto.queryCell = function (selector) {
    if (typeof selector == 'number') {
        // use number as index
        return this.cells[selector];
    }
    if (typeof selector == 'string') {
        // do not select invalid selectors from hash: #123, #/. #791
        if (selector.match(/^[#\.]?[\d\/]/)) {
            return;
        }
        // use string as selector, get element
        selector = this.element.querySelector(selector);
    }
    // get cell from element
    return this.getCell(selector);
};
// -------------------------- events -------------------------- //
proto.uiChange = function () {
    this.emitEvent('uiChange');
};
// keep focus on element when child UI elements are clicked
proto.childUIPointerDown = function (event) {
    // HACK iOS does not allow touch events to bubble up?!
    if (event.type != 'touchstart') {
        event.preventDefault();
    }
    this.focus();
};
// ----- resize ----- //
proto.onresize = function () {
    this.watchCSS();
    this.resize();
};
utils.debounceMethod(Flickity, 'onresize', 150);
proto.resize = function () {
    if (!this.isActive) {
        return;
    }
    this.getSize();
    // yo
    // wrap values
    if (this.options.wrapAround) {
        this.x = utils.modulo(this.x, this.slideableWidth);
    }
    this.positionCells();
    this._getWrapShiftCells();
    this.setGallerySize();
    this.emitEvent('resize');
    // update selected index for group slides, instant
    // TODO: position can be lost between groups of various numbers
    const selectedElement = this.selectedElements && this.selectedElements[0];
    this.selectCell(selectedElement, false, true);
};
// watches the :after property, activates/deactivates
proto.watchCSS = function () {
    const watchOption = this.options.watchCSS;
    if (!watchOption) {
        return;
    }
    const afterContent = getComputedStyle(this.element, ':after').content;
    // activate if :after { content: 'flickity' }
    if (afterContent.indexOf('flickity') != -1) {
        this.activate();
    }
    else {
        this.deactivate();
    }
};
// ----- keydown ----- //
// go previous/next if left/right keys pressed
proto.onkeydown = function (event) {
    // only work if element is in focus
    const isNotFocused = document.activeElement && document.activeElement != this.element;
    if (!this.options.accessibility || isNotFocused) {
        return;
    }
    const handler = Flickity.keyboardHandlers[event.keyCode];
    if (handler) {
        handler.call(this);
    }
};
Flickity.keyboardHandlers = {
    // left arrow
    37: function () {
        const leftMethod = this.options.rightToLeft ? 'next' : 'previous';
        this.uiChange();
        this[leftMethod]();
    },
    // right arrow
    39: function () {
        const rightMethod = this.options.rightToLeft ? 'previous' : 'next';
        this.uiChange();
        this[rightMethod]();
    },
};
// ----- focus ----- //
proto.focus = function () {
    // TODO remove scrollTo once focus options gets more support
    // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#Browser_compatibility
    const prevScrollY = window.pageYOffset;
    this.element.focus({ preventScroll: true });
    // hack to fix scroll jump after focus, #76
    if (window.pageYOffset != prevScrollY) {
        window.scrollTo(window.pageXOffset, prevScrollY);
    }
};
// -------------------------- destroy -------------------------- //
// deactivate all Flickity functionality, but keep stuff available
proto.deactivate = function () {
    if (!this.isActive) {
        return;
    }
    this.element.classList.remove('flickity-enabled');
    this.element.classList.remove('flickity-rtl');
    this.unselectSelectedSlide();
    // destroy cells
    this.cells.forEach(function (cell) {
        cell.destroy();
    });
    this.element.removeChild(this.viewport);
    // move child elements back into element
    moveElements(this.slider.children, this.element);
    if (this.options.accessibility) {
        this.element.removeAttribute('tabIndex');
        this.element.removeEventListener('keydown', this);
    }
    // set flags
    this.isActive = false;
    this.emitEvent('deactivate');
};
proto.destroy = function () {
    this.deactivate();
    window.removeEventListener('resize', this);
    this.allOff();
    this.emitEvent('destroy');
    delete this.element.flickityGUID;
    delete instances[this.guid];
};
// -------------------------- prototype -------------------------- //
utils.extend(proto, animatePrototype);
// -------------------------- extras -------------------------- //
/**
 * get Flickity instance from element
 * @param {Element} elem
 * @returns {Flickity} Flickity instance
 */
Flickity.data = function (elem) {
    elem = utils.getQueryElement(elem);
    const id = elem && elem.flickityGUID;
    return id && instances[id];
};
utils.htmlInit(Flickity, 'flickity');
Flickity.Cell = Cell;
Flickity.Slide = Slide;
