<commit_before>﻿/// <reference path="globals.ts"/>

import Entities = require("Framework/Signum.Web/Signum/Scripts/Entities")
import Validator = require("Framework/Signum.Web/Signum/Scripts/Validator")
import Navigator = require("Framework/Signum.Web/Signum/Scripts/Navigator")
import Finder = require("Framework/Signum.Web/Signum/Scripts/Finder")

export interface EntityBaseOptions {
    prefix: string;
    partialViewName: string;
    template?: string;
    templateToString?: string;

    autoCompleteUrl?: string;

    types: Entities.TypeInfo[]; 

    isEmbedded: boolean;
    isReadonly: boolean;
    rootType?: string;
    propertyRoute?: string;
}

export class EntityBase {
    prefix: string;
    options: EntityBaseOptions;
    element: JQuery;
    hidden: JQuery;
    inputGroup: JQuery;
    shownButton: JQuery;
    autoCompleter: EntityAutocompleter;

    entityChanged: () => void;
    removing: (prefix: string) => Promise<boolean>;
    creating: (prefix: string) => Promise<Entities.EntityValue>;
    finding: (prefix: string) => Promise<Entities.EntityValue>;
    viewing: (entityHtml: Entities.EntityHtml) => Promise<Entities.EntityValue>;

    constructor(element: JQuery, options: EntityBaseOptions) {
        this.element = element;
        this.element.data("SF-control", this);
        this.options = options;
        this.prefix = this.options.prefix;
        this.hidden =  this.prefix.child("hidden").tryGet();
        this.inputGroup = this.prefix.child("inputGroup").tryGet();
        this.shownButton = this.prefix.child("shownButton").tryGet();
      
        var temp = this.prefix.child(Entities.Keys.template).tryGet();

        if (temp.length > 0) {
            this.options.template = temp.html().replaceAll("<scriptX", "<script").replaceAll("</scriptX", "</script");
            this.options.templateToString = temp.attr("data-toString");
        }

        this.fixInputGroup();

        this._create();
    }

    public ready() {

        this.element.SFControlFullfill(this);
    }

    static key_entity = "sfEntity";

    _create() { //abstract

    }

    runtimeInfoHiddenElement(itemPrefix?: string): JQuery {
        return this.prefix.child(Entities.Keys.runtimeInfo).get();
    }

    containerDiv(itemPrefix?: string) {
        var containerDivId = this.prefix.child(EntityBase.key_entity);
        var result = containerDivId.tryGet(); 
        if (result.length)
            return result;

        return SF.hiddenDiv(containerDivId, "").insertAfter(this.runtimeInfoHiddenElement()); 
    }

    getRuntimeInfo(): Entities.RuntimeInfo
    {
        return Entities.RuntimeInfo.getFromPrefix(this.options.prefix);
    }

    extractEntityHtml(itemPrefix?: string): Entities.EntityHtml {

        var runtimeInfo = Entities.RuntimeInfo.getFromPrefix(this.options.prefix);

        if (runtimeInfo == null)
            return null;

        var div = this.containerDiv();

        var result = new Entities.EntityHtml(this.options.prefix, runtimeInfo,
            this.getToString(),
            this.getLink());

        result.html = div.children();

        div.html(null);

        return result;
    }

    getOrRequestEntityHtml() : Promise<Entities.EntityHtml>
    {
        var runtimeInfo = Entities.RuntimeInfo.getFromPrefix(this.options.prefix);

        if (runtimeInfo == null)
            return Promise.resolve(null);

        var div = this.containerDiv();

        var result = new Entities.EntityHtml(this.options.prefix, runtimeInfo,
            this.getToString(),
            this.getLink());

        result.html = div.children();

        if (result.isLoaded())
            return Promise.resolve(result); 

        return Navigator.requestPartialView(result, this.defaultViewOptions(null));
    }

    getLink(itemPrefix?: string): string {
        return null;
    }

    getToString(itemPrefix?: string): string {
        return null;
    }


    setEntitySpecific(entityValue: Entities.EntityValue, itemPrefix?: string) {
        //virtual function
    }

    setEntity(entityValue: Entities.EntityValue, itemPrefix?: string) {

        this.setEntitySpecific(entityValue)

        if (entityValue)
            entityValue.assertPrefixAndType(this.options.prefix, this.options.types);


        this.containerDiv().html(entityValue == null ? null : (<Entities.EntityHtml>entityValue).html);
        Entities.RuntimeInfo.setFromPrefix(this.options.prefix, entityValue == null ? null : entityValue.runtimeInfo);
        if (entityValue == null) {
            Validator.cleanHasError(this.element);
        }

        this.updateButtonsDisplay();
        this.notifyChanges(true);
        if (!SF.isEmpty(this.entityChanged)) {
            this.entityChanged();
        }
    }

    notifyChanges(setHasChanges: boolean) {
        if (setHasChanges)
            SF.setHasChanges(this.element);

        this.element.attr("changes", (parseInt(this.element.attr("changes")) || 0) + 1);
    }

    remove_click(): Promise<string> {
        return this.onRemove(this.options.prefix).then(result=> {
            if (result) {
                this.setEntity(null);
                return this.options.prefix;
            }

            return null;
        });
    }

    onRemove(prefix: string): Promise<boolean> {
        if (this.removing != null)
            return this.removing(prefix);

        return Promise.resolve(true);
    }

    create_click(): Promise<string> {
        return this.onCreating(this.options.prefix).then(result => {
            if (result) {
                this.setEntity(result);
                return this.options.prefix;
            }

            this.notifyChanges(false);
            return null;
        });
    }

    typeChooser(filter: (type: Entities.TypeInfo) => boolean): Promise<Entities.TypeInfo> {
        return Navigator.typeChooser(this.options.prefix, this.options.types.filter(filter));
    }

    singleType(): string {
        if (this.options.types.length != 1)
            throw new Error("There are {0} types in {1}".format(this.options.types.length, this.options.prefix));

        return this.options.types[0].name;
    }

    onCreating(prefix: string): Promise<Entities.EntityValue> {
        if (this.creating != null)
            return this.creating(prefix);

        return this.typeChooser(ti => ti.creable).then(type=> {
            if (!type)
                return null;

            return type.preConstruct().then(extra => {

                if (!extra)
                    return null;

                var newEntity = this.options.template ? this.getEmbeddedTemplate(prefix) :
                    new Entities.EntityHtml(prefix, new Entities.RuntimeInfo(type.name, null, true), lang.signum.newEntity);

                return Navigator.viewPopup(newEntity, this.defaultViewOptions(extra));
            });
        });
    }

    getEmbeddedTemplate(itemPrefix?: string): Entities.EntityHtml {
        if (!this.options.template)
            throw new Error("no template in " + this.options.prefix);

        var result = new Entities.EntityHtml(this.options.prefix,
            new Entities.RuntimeInfo(this.singleType(), null, true), this.options.templateToString);

        result.loadHtml(this.options.template);

        return result;
    }

    view_click(): Promise<string> {
        var entityHtml = this.extractEntityHtml();

        return this.onViewing(entityHtml).then(result=> {
            if (result) {
                this.setEntity(result);
                return this.options.prefix;
            }
            else {
                this.setEntity(entityHtml); //previous entity passed by reference
                return null;
            }
        });
    }

    onViewing(entityHtml: Entities.EntityHtml): Promise<Entities.EntityValue> {
        if (this.viewing != null)
            return this.viewing(entityHtml);

        return Navigator.viewPopup(entityHtml, this.defaultViewOptions(null));
    }

    find_click(): Promise<string> {
        return this.onFinding(this.options.prefix).then(result => {
            if (result) {
                this.setEntity(result);
                return this.options.prefix;
            }

            this.notifyChanges(false);
            return null;
        });
    }



    onFinding(prefix: string): Promise<Entities.EntityValue> {
        if (this.finding != null)
            return this.finding(prefix);

        return this.typeChooser(ti => ti.findable).then(type=> {
            if (!type)
                return null;

            return Finder.find({
                webQueryName: type.name,
                prefix: prefix,
            });
        });
    }

    defaultViewOptions(extraJsonData: any): Navigator.ViewPopupOptions {
        return {
            readOnly: this.options.isReadonly,
            partialViewName: this.options.partialViewName,
            validationOptions: {
                rootType: this.options.rootType,
                propertyRoute: this.options.propertyRoute,
            },
            requestExtraJsonData : extraJsonData,
        };
    }

    updateButtonsDisplay() {

        var hasEntity = !!Entities.RuntimeInfo.getFromPrefix(this.options.prefix);

        this.visibleButton("btnCreate", !hasEntity);
        this.visibleButton("btnFind", !hasEntity);
        this.visibleButton("btnView", hasEntity);
        this.visibleButton("btnRemove", hasEntity);

        this.fixInputGroup();
    }

    fixInputGroup() {
        this.inputGroup.toggleClass("input-group", !!this.shownButton.children().length);
    }

    visibleButton(sufix: string, visible: boolean) {

        var element = this.prefix.child(sufix).tryGet();

        if (!element.length)
            return;

        (visible ? this.shownButton : this.hidden).append(element.detach());
    }

    setupAutocomplete($txt : JQuery) {

        var handler : number;
        var auto = $txt.typeahead({
            source: (query, response) => {
                if (handler)
                    clearTimeout(handler);

                handler = setTimeout(() => {
                    this.autoCompleter.getResults(query)
                        .then(entities=> response(entities));
                }, 300);
            },

            sorter: items => items,
            matcher: item => true,
            highlighter: (item: Entities.EntityValue) => $("<div>").append(
                $("<span>")
                    .attr("data-type", item.runtimeInfo.type)
                    .attr("data-id", item.runtimeInfo.id)
                    .text(item.toStr)).html(),

            updater : (val: Entities.EntityValue) => this.onAutocompleteSelected(val)
        });
    }

    onAutocompleteSelected(entityValue: Entities.EntityValue) {
        throw new Error("onAutocompleteSelected is abstract");
    }

    getNiceName(typeName: string) : string {

        var t = this.options.types.filter(a=> a.name == typeName);

        return t.length ? t[0].niceName : typeName;
    }
}

export interface EntityAutocompleter {
    getResults(term: string): Promise<Entities.EntityValue[]>;
}

export interface AutocompleteResult {
    id: number;
    text: string;
    type: string;
    link: string;
}

export class AjaxEntityAutocompleter implements EntityAutocompleter {

    controllerUrl: string;

    getData: (term: string) => any;

    constructor(controllerUrl: string, getData: (term: string) => any) {
        this.controllerUrl = controllerUrl;
        this.getData = getData;
    }

    lastXhr: JQueryXHR; //To avoid previous requests results to be shown

    getResults(term: string): Promise<Entities.EntityValue[]> {
        if (this.lastXhr)
            this.lastXhr.abort();

        return new Promise<Entities.EntityValue[]>((resolve, failure) => {
            this.lastXhr = $.ajax({
                url: this.controllerUrl,
                data: this.getData(term),
                success: function (data: AutocompleteResult[]) {
                    this.lastXhr = null;
                    var entities = data.map(item=> new Entities.EntityValue(new Entities.RuntimeInfo(item.type, item.id, false), item.text, item.link));
                    resolve(entities);
                }
            });
        });
    }

}

export class EntityLine extends EntityBase {

    _create() {
        var $txt = this.prefix.child(Entities.Keys.toStr).tryGet().filter(".sf-entity-autocomplete");
        if ($txt.length) {
            this.autoCompleter = new AjaxEntityAutocompleter(this.options.autoCompleteUrl || SF.Urls.autocomplete,
                term => ({ types: this.options.types.map(t=> t.name).join(","), l: 5, q: term }));

            this.setupAutocomplete($txt);
        }
    }

    getLink(itemPrefix?: string): string {
        return this.prefix.child(Entities.Keys.link).get().attr("href");
    }

    getToString(itemPrefix?: string): string {
        return this.prefix.child(Entities.Keys.link).get().text();
    }

    setEntitySpecific(entityValue: Entities.EntityValue, itemPrefix?: string) {
        var link = this.prefix.child(Entities.Keys.link).get();
        link.text(entityValue == null ? null : entityValue.toStr);
        if (link.is('a'))
            link.attr('href', entityValue == null ? null : entityValue.link);
        this.prefix.child(Entities.Keys.toStr).get().val('');
        
        this.visible(this.prefix.child(Entities.Keys.link).tryGet(), entityValue != null);
        this.visible(this.prefix.get().find("ul.typeahead.dropdown-menu"), entityValue == null);
        this.visible(this.prefix.child(Entities.Keys.toStr).tryGet(), entityValue == null);
    }

    visible(element : JQuery, visible: boolean) {
        if (!element.length)
            return;

        if (visible)
            this.shownButton.before(element.detach());
        else
            this.hidden.append(element.detach());
    }

    onAutocompleteSelected(entityValue: Entities.EntityValue) {
        this.setEntity(entityValue);
    }
}

export class EntityCombo extends EntityBase {

    static key_combo = "sfCombo";

    combo() {
        return this.prefix.child(EntityCombo.key_combo).get();
    }

    setEntitySpecific(entityValue: Entities.EntityValue) {
        var c = this.combo();

        if (entityValue == null)
            c.val(null);
        else {
            var o = c.children("option[value='" + entityValue.runtimeInfo.key() + "']");
            if (o.length == 1)
                o.html(entityValue.toStr);
            else
                c.add($("<option value='{0}'/>".format(entityValue.runtimeInfo.key())).text(entityValue.toStr));

            c.val(entityValue.runtimeInfo.key());
        }
    }

    getToString(itemPrefix?: string): string {
        return this.combo().children("option[value='" + this.combo().val() + "']").text();
    }

    combo_selected() {
        var val = this.combo().val();

        var ri = Entities.RuntimeInfo.fromKey(val);

        this.setEntity(ri == null ? null : new Entities.EntityValue(ri, this.getToString()));
    }
}

export class EntityLineDetail extends EntityBase {

    options: EntityBaseOptions;

    constructor(element: JQuery, options: EntityBaseOptions) {
        super(element, options);
    }

    containerDiv(itemPrefix?: string) {
        return this.prefix.child("sfDetail").get();
    }

    fixInputGroup() {
    }

    setEntitySpecific(entityValue: Entities.EntityValue, itemPrefix?: string) {
        if (entityValue == null)
            return;

        if (!entityValue.isLoaded())
            throw new Error("EntityLineDetail requires a loaded Entities.EntityHtml, consider calling Navigator.loadPartialView");
    }

    onCreating(prefix: string): Promise<Entities.EntityValue> {
        if (this.creating != null)
            return this.creating(prefix);

        if (this.options.template)
            return Promise.resolve(this.getEmbeddedTemplate(prefix));

        return this.typeChooser(t=>t.creable).then(type=> {
            if (!type)
                return null;

            return type.preConstruct().then(args=> {
                if (!args)
                    return null;

                var newEntity = new Entities.EntityHtml(prefix, new Entities.RuntimeInfo(type.name, null, true), lang.signum.newEntity);

                return Navigator.requestPartialView(newEntity, this.defaultViewOptions(args));
            });
        });
    }

    find_click(): Promise<string> {
        return this.onFinding(this.options.prefix).then(result => {
            if (result == null) {
                this.notifyChanges(false);
                return null;
            }

            if (result.isLoaded())
                return Promise.resolve(<Entities.EntityHtml>result);

            return Navigator.requestPartialView(new Entities.EntityHtml(this.options.prefix, result.runtimeInfo), this.defaultViewOptions(null));
        }).then(result => {
                if (result) {
                    this.setEntity(result);
                    return this.options.prefix;
                }

                return null;
            });
    }
}



export interface EntityListBaseOptions extends EntityBaseOptions {
    maxElements?: number;
    remove?: boolean;
    reorder?: boolean;
}

export class EntityListBase extends EntityBase {
    static key_index = "sfIndex";
    static key_rowId = "sfRowId";

    options: EntityListBaseOptions;
    finding: (prefix: string) => Promise<Entities.EntityValue>;  // DEPRECATED!
    findingMany: (prefix: string) => Promise<Entities.EntityValue[]>;

    constructor(element: JQuery, options: EntityListBaseOptions) {
        super(element, options);
    }

    runtimeInfo(itemPrefix?: string): JQuery {
        return itemPrefix.child(Entities.Keys.runtimeInfo).get();
    }

    containerDiv(itemPrefix?: string): JQuery {
        var containerDivId = itemPrefix.child(EntityList.key_entity);

        var result = containerDivId.tryGet();

        if (result.length)
            return result;

        return SF.hiddenDiv(containerDivId, "").insertAfter(this.runtimeInfo(itemPrefix));
    }

    getEmbeddedTemplate(itemPrefix?: string) {
        if (!this.options.template)
            throw new Error("no template in " + this.options.prefix);

        var result = new Entities.EntityHtml(itemPrefix,
            new Entities.RuntimeInfo(this.singleType(), null, true), this.options.templateToString);

        var replaced = this.options.template.replace(new RegExp(this.options.prefix.child("0"), "gi"), itemPrefix)

        result.loadHtml(replaced);

        return result;
    }

    extractEntityHtml(itemPrefix?: string): Entities.EntityHtml {
        var runtimeInfo = Entities.RuntimeInfo.getFromPrefix(itemPrefix);

        var div = this.containerDiv(itemPrefix);

        var result = new Entities.EntityHtml(itemPrefix, runtimeInfo,
            this.getToString(itemPrefix),
            this.getLink(itemPrefix));

        result.html = div.children();

        div.html(null);

        return result;
    }

    setEntity(entityValue: Entities.EntityValue, itemPrefix?: string) {

        if (entityValue == null)
            throw new Error("entityValue is mandatory on setEntityItem");

        this.setEntitySpecific(entityValue, itemPrefix)

        entityValue.assertPrefixAndType(itemPrefix, this.options.types);

        if (entityValue.isLoaded())
            this.containerDiv(itemPrefix).html((<Entities.EntityHtml>entityValue).html);

        Entities.RuntimeInfo.setFromPrefix(itemPrefix, entityValue.runtimeInfo);

        this.updateButtonsDisplay();
        this.notifyChanges(true);
        if (!SF.isEmpty(this.entityChanged)) {
            this.entityChanged();
        }
    }

    create_click(): Promise<string> {
        var itemPrefix = this.reserveNextPrefix();
        return this.onCreating(itemPrefix).then(entity => {
            if (entity) {
                this.addEntity(entity, itemPrefix);
                return itemPrefix;
            }

            this.notifyChanges(false);
            return null;
        }).then(
            prefix => { this.freeReservedPrefix(itemPrefix); return prefix; },
            error => { this.freeReservedPrefix(itemPrefix); throw error; return ""; });
    }

    addEntitySpecific(entityValue: Entities.EntityValue, itemPrefix: string) {
        //virtual
    }

    addEntity(entityValue: Entities.EntityValue, itemPrefix: string) {
        if (entityValue == null)
            throw new Error("entityValue is mandatory on setEntityItem");

        this.addEntitySpecific(entityValue, itemPrefix);

        if (entityValue)
            entityValue.assertPrefixAndType(itemPrefix, this.options.types);

        if (entityValue.isLoaded())
            this.containerDiv(itemPrefix).html((<Entities.EntityHtml>entityValue).html);
        Entities.RuntimeInfo.setFromPrefix(itemPrefix, entityValue.runtimeInfo);

        this.updateButtonsDisplay();
        this.notifyChanges(true);
        if (!SF.isEmpty(this.entityChanged)) {
            this.entityChanged();
        }
    }

    removeEntitySpecific(itemPrefix: string) {
        //virtual
    }

    removeEntity(itemPrefix: string) {
        this.removeEntitySpecific(itemPrefix);

        this.updateButtonsDisplay();
        this.notifyChanges(true);
        if (!SF.isEmpty(this.entityChanged)) {
            this.entityChanged();
        }
    }

    itemSuffix(): string {
        throw new Error("itemSuffix is abstract");
    }

    getItems(): JQuery {
        throw new Error("getItems is abstract");
    }

    getPrefixes(): string[] {
        return this.getItems().toArray()
            .map((e: HTMLElement) => e.id.before("_" + this.itemSuffix()));
    }

    getRuntimeInfos(): Entities.RuntimeInfo[] {
        return this.getPrefixes().map(p=> Entities.RuntimeInfo.getFromPrefix(p));
    }

    reservedPrefixes: string[] = []; 

    reserveNextPrefix(): string {

        var currentPrefixes = this.getItems().toArray().map((e: HTMLElement) => e.id.before("_" + this.itemSuffix()));

        for (var i = 0; ; i++)
        {
            var newPrefix = this.options.prefix + "_" + i;

            if (this.reservedPrefixes.indexOf(newPrefix) == -1 &&
                currentPrefixes.indexOf(newPrefix) == -1) {

                this.reservedPrefixes.push(newPrefix);

                return newPrefix;
            }
        }
    }

    freeReservedPrefix(itemPrefix : string) {
        var index = this.reservedPrefixes.indexOf(itemPrefix);
        if (index == -1)
            throw Error("itemPrefix not reserved: " + itemPrefix);

        return this.reservedPrefixes.splice(index, 1);
    }

    getLastPosIndex(): number {
        var $last = this.getItems().filter(":last");
        if ($last.length == 0) {
            return -1;
        }

        var lastId = $last[0].id;
        var lastPrefix = lastId.substring(0, lastId.indexOf(this.itemSuffix()) - 1);

        return this.getPosIndex(lastPrefix);
    }

    getNextPosIndex(): string {
        return (this.getLastPosIndex() + 1).toString();
    }

    canAddItems() {
        return SF.isEmpty(this.options.maxElements) || this.getItems().length < this.options.maxElements;
    }

    find_click(): Promise<string> {

        var prefixes = [];

        return this.onFindingMany(this.options.prefix).then(result => {
            if (result) {

                result.forEach(ev=> {
                    var pr = this.reserveNextPrefix();
                    prefixes.push(pr);
                    this.addEntity(ev, pr);
                });

                return prefixes.join(",");
            }

            this.notifyChanges(false);
            return null;
        }).then(
            prefix => { prefixes.forEach(this.freeReservedPrefix); return prefix; },
            error => { prefixes.forEach(this.freeReservedPrefix); throw error; return ""; });
    }

    onFinding(prefix: string): Promise<Entities.EntityValue> {
        throw new Error("onFinding is deprecated in EntityListBase");
    }

    onFindingMany(prefix: string): Promise<Entities.EntityValue[]> {
        if (this.findingMany != null)
            return this.findingMany(prefix);

        return this.typeChooser(t => t.findable).then(type=> {
            if (type == null)
                return null;

            return Finder.findMany({
                webQueryName: type.name,
                prefix: prefix,
            });
        });
    }

    moveUp(itemPrefix: string) {

        var suffix = this.itemSuffix();
        var $item = itemPrefix.child(suffix).get();
        var $itemPrev = $item.prev();

        if ($itemPrev.length == 0) {
            return;
        }

        var itemPrevPrefix = $itemPrev[0].id.parent(suffix);

        var prevNewIndex = this.getPosIndex(itemPrevPrefix);
        this.setPosIndex(itemPrefix, prevNewIndex);
        this.setPosIndex(itemPrevPrefix, prevNewIndex + 1);

        $item.insertBefore($itemPrev);

        this.notifyChanges(true);
    }

    moveDown(itemPrefix: string) {

        var suffix = this.itemSuffix();
        var $item = itemPrefix.child(suffix).get();
        var $itemNext = $item.next();

        if ($itemNext.length == 0) {
            return;
        }

        var itemNextPrefix = $itemNext[0].id.parent(suffix);

        var nextNewIndex = this.getPosIndex(itemNextPrefix);
        this.setPosIndex(itemPrefix, nextNewIndex);
        this.setPosIndex(itemNextPrefix, nextNewIndex - 1);

        $item.insertAfter($itemNext);

        this.notifyChanges(true);
    }

    getPosIndex(itemPrefix: string) {
        return parseInt(itemPrefix.child(EntityListBase.key_index).get().val());
    }

    setPosIndex(itemPrefix: string, newIndex: number) {
        var $indexes = itemPrefix.child(EntityListBase.key_index).get().val(newIndex.toString());
    }
}

export class EntityList extends EntityListBase {

    static key_list = "sfList";

    _create() {
        var list = this.prefix.child(EntityList.key_list).get();

        list.change(() => this.selection_Changed());

        SF.onVisible(list).then(() => {
            if (list.height() < this.shownButton.height())
                list.css("min-height", this.shownButton.height());
        });

        this.selection_Changed();
    }

    selection_Changed() {
        this.updateButtonsDisplay();
    }

    itemSuffix() {
        return Entities.Keys.toStr;
    }

    updateLinks(newToStr: string, newLink: string, itemPrefix?: string) {
        itemPrefix.child(Entities.Keys.toStr).get().html(newToStr);
    }

    selectedItemPrefix(): string {
        var $items = this.getItems().filter(":selected");
        if ($items.length == 0) {
            return null;
        }

        var nameSelected = $items[0].id;
        return nameSelected.before("_" + this.itemSuffix());
    }

    getItems(): JQuery {
        return this.prefix.child(EntityList.key_list).get().children("option");
    }

    view_click(): Promise<string> {
        var selectedItemPrefix = this.selectedItemPrefix();

        var entityHtml = this.extractEntityHtml(selectedItemPrefix);

        return this.onViewing(entityHtml).then(result=> {
            if (result) {
                this.setEntity(result, selectedItemPrefix);
                return selectedItemPrefix;
            }
            else {
                this.setEntity(entityHtml, selectedItemPrefix); //previous entity passed by reference
                return null;
            }
        });
    }



    updateButtonsDisplay() {
        var canAdd = this.canAddItems();
        this.visibleButton("btnCreate", canAdd);
        this.visibleButton("btnFind", canAdd);

        var hasSelected = this.selectedItemPrefix() != null;
        this.visibleButton("btnView", hasSelected);
        this.visibleButton("btnRemove", hasSelected);
        this.visibleButton("btnUp", hasSelected);
        this.visibleButton("btnDown", hasSelected);

        this.fixInputGroup();
    }

    getToString(itemPrefix?: string): string {
        return itemPrefix.child(Entities.Keys.toStr).get().text();
    }

    setEntitySpecific(entityValue: Entities.EntityValue, itemPrefix?: string) {
        itemPrefix.child(Entities.Keys.toStr).get().text(entityValue.toStr);
    }

    addEntitySpecific(entityValue: Entities.EntityValue, itemPrefix: string) {

        this.inputGroup.before(SF.hiddenInput(itemPrefix.child(EntityList.key_index), this.getNextPosIndex()));
        this.inputGroup.before(SF.hiddenInput(itemPrefix.child(EntityList.key_rowId), ""));
        this.inputGroup.before(SF.hiddenInput(itemPrefix.child(Entities.Keys.runtimeInfo), entityValue.runtimeInfo.toString()));
        this.inputGroup.before(SF.hiddenDiv(itemPrefix.child(EntityList.key_entity), ""));

        var select = this.prefix.child(EntityList.key_list).get();
        select.children('option').attr('selected', false); //Fix for Firefox: Set selected after retrieving the html of the select

        var ri = entityValue.runtimeInfo;

        $("<option/>")
            .attr("id", itemPrefix.child(Entities.Keys.toStr))
            .attr("value", "")
            .attr('selected', true)
            .text(entityValue.toStr)
            .attr('title', this.options.isEmbedded ? null : (this.getNiceName(ri.type) + (ri.id ? " " + ri.id : null)))
            .appendTo(select);
    }

    remove_click(): Promise<string> {
        var selectedItemPrefix = this.selectedItemPrefix();
        return this.onRemove(selectedItemPrefix).then(result=> {
            if (result) {
                var next = this.getItems().filter(":selected").next();
                if (next.length == 0)
                    next = this.getItems().filter(":selected").prev();

                this.removeEntity(selectedItemPrefix);

                next.attr("selected", "selected");
                this.selection_Changed();

                return selectedItemPrefix;
            }

            return null;
        });
    }

    removeEntitySpecific(itemPrefix: string) {
        itemPrefix.child(Entities.Keys.runtimeInfo).get().remove();
        itemPrefix.child(Entities.Keys.toStr).get().remove();
        itemPrefix.child(EntityList.key_entity).tryGet().remove();
        itemPrefix.child(EntityList.key_index).tryGet().remove();
        itemPrefix.child(EntityList.key_rowId).tryGet().remove();
    }

    moveUp_click() {
        this.moveUp(this.selectedItemPrefix());
    }

    moveDown_click() {
        this.moveDown(this.selectedItemPrefix());
    }
}

export interface EntityListDetailOptions extends EntityListBaseOptions {
    detailDiv: string;
}

export class EntityListDetail extends EntityList {

    options: EntityListDetailOptions;

    selection_Changed() {
        super.selection_Changed();
        this.stageCurrentSelected();
    }

    remove_click(): Promise<string>  {
        return super.remove_click().then(result => { this.stageCurrentSelected(); return result })
    }

    create_click() {
        return super.create_click().then(result => { this.stageCurrentSelected(); return result; });
    }

    find_click() {
        return super.find_click().then(result => { this.stageCurrentSelected(); return result; })
    }

    stageCurrentSelected() {
        var selPrefix = this.selectedItemPrefix();

        var detailDiv = $("#" + this.options.detailDiv)

        var currentChildren = detailDiv.children();
        var currentPrefix = currentChildren.length ? currentChildren[0].id.parent(EntityListDetail.key_entity) : null;
        if (currentPrefix == selPrefix) {
            return;
        }

        var hideCurrent = () => {

            if (currentPrefix) {
                currentChildren.hide();
                this.runtimeInfo(currentPrefix).after(currentChildren);
            }
        }; 

        if (selPrefix) {
            var selContainer = this.containerDiv(selPrefix);

            var promise = selContainer.children().length ? Promise.resolve<void>(null) :
                Navigator.requestPartialView(new Entities.EntityHtml(selPrefix, Entities.RuntimeInfo.getFromPrefix(selPrefix), null, null))
                    .then<void>(e=> selContainer.html(e.html));

            promise.then(() =>
            {
                detailDiv.append(selContainer);
                selContainer.show();

                if (currentPrefix) {
                    currentChildren.hide();
                    this.runtimeInfo(currentPrefix).after(currentChildren);
                }
            }); <commit_msg>PartialView default available<commit_after>﻿/// <reference path="globals.ts"/>

import Entities = require("Framework/Signum.Web/Signum/Scripts/Entities")
import Validator = require("Framework/Signum.Web/Signum/Scripts/Validator")
import Navigator = require("Framework/Signum.Web/Signum/Scripts/Navigator")
import Finder = require("Framework/Signum.Web/Signum/Scripts/Finder")

export interface EntityBaseOptions {
    prefix: string;
    partialViewName: string;
    template?: string;
    templateToString?: string;

    autoCompleteUrl?: string;

    types: Entities.TypeInfo[]; 

    isEmbedded: boolean;
    isReadonly: boolean;
    rootType?: string;
    propertyRoute?: string;
}

export class EntityBase {
    prefix: string;
    options: EntityBaseOptions;
    element: JQuery;
    hidden: JQuery;
    inputGroup: JQuery;
    shownButton: JQuery;
    autoCompleter: EntityAutocompleter;

    entityChanged: () => void;
    removing: (prefix: string) => Promise<boolean>;
    creating: (prefix: string) => Promise<Entities.EntityValue>;
    finding: (prefix: string) => Promise<Entities.EntityValue>;
    viewing: (entityHtml: Entities.EntityHtml) => Promise<Entities.EntityValue>;

    constructor(element: JQuery, options: EntityBaseOptions) {
        this.element = element;
        this.element.data("SF-control", this);
        this.options = options;
        this.prefix = this.options.prefix;
        this.hidden =  this.prefix.child("hidden").tryGet();
        this.inputGroup = this.prefix.child("inputGroup").tryGet();
        this.shownButton = this.prefix.child("shownButton").tryGet();
      
        var temp = this.prefix.child(Entities.Keys.template).tryGet();

        if (temp.length > 0) {
            this.options.template = temp.html().replaceAll("<scriptX", "<script").replaceAll("</scriptX", "</script");
            this.options.templateToString = temp.attr("data-toString");
        }

        this.fixInputGroup();

        this._create();
    }

    public ready() {

        this.element.SFControlFullfill(this);
    }

    static key_entity = "sfEntity";

    _create() { //abstract

    }

    runtimeInfoHiddenElement(itemPrefix?: string): JQuery {
        return this.prefix.child(Entities.Keys.runtimeInfo).get();
    }

    containerDiv(itemPrefix?: string) {
        var containerDivId = this.prefix.child(EntityBase.key_entity);
        var result = containerDivId.tryGet(); 
        if (result.length)
            return result;

        return SF.hiddenDiv(containerDivId, "").insertAfter(this.runtimeInfoHiddenElement()); 
    }

    getRuntimeInfo(): Entities.RuntimeInfo
    {
        return Entities.RuntimeInfo.getFromPrefix(this.options.prefix);
    }

    extractEntityHtml(itemPrefix?: string): Entities.EntityHtml {

        var runtimeInfo = Entities.RuntimeInfo.getFromPrefix(this.options.prefix);

        if (runtimeInfo == null)
            return null;

        var div = this.containerDiv();

        var result = new Entities.EntityHtml(this.options.prefix, runtimeInfo,
            this.getToString(),
            this.getLink());

        result.html = div.children();

        div.html(null);

        return result;
    }

    getOrRequestEntityHtml() : Promise<Entities.EntityHtml>
    {
        var runtimeInfo = Entities.RuntimeInfo.getFromPrefix(this.options.prefix);

        if (runtimeInfo == null)
            return Promise.resolve(null);

        var div = this.containerDiv();

        var result = new Entities.EntityHtml(this.options.prefix, runtimeInfo,
            this.getToString(),
            this.getLink());

        result.html = div.children();

        if (result.isLoaded())
            return Promise.resolve(result); 

        return Navigator.requestPartialView(result, this.defaultViewOptions(null));
    }

    getLink(itemPrefix?: string): string {
        return null;
    }

    getToString(itemPrefix?: string): string {
        return null;
    }


    setEntitySpecific(entityValue: Entities.EntityValue, itemPrefix?: string) {
        //virtual function
    }

    setEntity(entityValue: Entities.EntityValue, itemPrefix?: string) {

        this.setEntitySpecific(entityValue)

        if (entityValue)
            entityValue.assertPrefixAndType(this.options.prefix, this.options.types);


        this.containerDiv().html(entityValue == null ? null : (<Entities.EntityHtml>entityValue).html);
        Entities.RuntimeInfo.setFromPrefix(this.options.prefix, entityValue == null ? null : entityValue.runtimeInfo);
        if (entityValue == null) {
            Validator.cleanHasError(this.element);
        }

        this.updateButtonsDisplay();
        this.notifyChanges(true);
        if (!SF.isEmpty(this.entityChanged)) {
            this.entityChanged();
        }
    }

    notifyChanges(setHasChanges: boolean) {
        if (setHasChanges)
            SF.setHasChanges(this.element);

        this.element.attr("changes", (parseInt(this.element.attr("changes")) || 0) + 1);
    }

    remove_click(): Promise<string> {
        return this.onRemove(this.options.prefix).then(result=> {
            if (result) {
                this.setEntity(null);
                return this.options.prefix;
            }

            return null;
        });
    }

    onRemove(prefix: string): Promise<boolean> {
        if (this.removing != null)
            return this.removing(prefix);

        return Promise.resolve(true);
    }

    create_click(): Promise<string> {
        return this.onCreating(this.options.prefix).then(result => {
            if (result) {
                this.setEntity(result);
                return this.options.prefix;
            }

            this.notifyChanges(false);
            return null;
        });
    }

    typeChooser(filter: (type: Entities.TypeInfo) => boolean): Promise<Entities.TypeInfo> {
        return Navigator.typeChooser(this.options.prefix, this.options.types.filter(filter));
    }

    singleType(): string {
        if (this.options.types.length != 1)
            throw new Error("There are {0} types in {1}".format(this.options.types.length, this.options.prefix));

        return this.options.types[0].name;
    }

    onCreating(prefix: string): Promise<Entities.EntityValue> {
        if (this.creating != null)
            return this.creating(prefix);

        return this.typeChooser(ti => ti.creable).then(type=> {
            if (!type)
                return null;

            return type.preConstruct().then(extra => {

                if (!extra)
                    return null;

                var newEntity = this.options.template ? this.getEmbeddedTemplate(prefix) :
                    new Entities.EntityHtml(prefix, new Entities.RuntimeInfo(type.name, null, true), lang.signum.newEntity);

                return Navigator.viewPopup(newEntity, this.defaultViewOptions(extra));
            });
        });
    }

    getEmbeddedTemplate(itemPrefix?: string): Entities.EntityHtml {
        if (!this.options.template)
            throw new Error("no template in " + this.options.prefix);

        var result = new Entities.EntityHtml(this.options.prefix,
            new Entities.RuntimeInfo(this.singleType(), null, true), this.options.templateToString);

        result.loadHtml(this.options.template);

        return result;
    }

    view_click(): Promise<string> {
        var entityHtml = this.extractEntityHtml();

        return this.onViewing(entityHtml).then(result=> {
            if (result) {
                this.setEntity(result);
                return this.options.prefix;
            }
            else {
                this.setEntity(entityHtml); //previous entity passed by reference
                return null;
            }
        });
    }

    onViewing(entityHtml: Entities.EntityHtml): Promise<Entities.EntityValue> {
        if (this.viewing != null)
            return this.viewing(entityHtml);

        return Navigator.viewPopup(entityHtml, this.defaultViewOptions(null));
    }

    find_click(): Promise<string> {
        return this.onFinding(this.options.prefix).then(result => {
            if (result) {
                this.setEntity(result);
                return this.options.prefix;
            }

            this.notifyChanges(false);
            return null;
        });
    }



    onFinding(prefix: string): Promise<Entities.EntityValue> {
        if (this.finding != null)
            return this.finding(prefix);

        return this.typeChooser(ti => ti.findable).then(type=> {
            if (!type)
                return null;

            return Finder.find({
                webQueryName: type.name,
                prefix: prefix,
            });
        });
    }

    defaultViewOptions(extraJsonData: any): Navigator.ViewPopupOptions {
        return {
            readOnly: this.options.isReadonly,
            partialViewName: this.options.partialViewName,
            validationOptions: {
                rootType: this.options.rootType,
                propertyRoute: this.options.propertyRoute,
            },
            requestExtraJsonData : extraJsonData,
        };
    }

    updateButtonsDisplay() {

        var hasEntity = !!Entities.RuntimeInfo.getFromPrefix(this.options.prefix);

        this.visibleButton("btnCreate", !hasEntity);
        this.visibleButton("btnFind", !hasEntity);
        this.visibleButton("btnView", hasEntity);
        this.visibleButton("btnRemove", hasEntity);

        this.fixInputGroup();
    }

    fixInputGroup() {
        this.inputGroup.toggleClass("input-group", !!this.shownButton.children().length);
    }

    visibleButton(sufix: string, visible: boolean) {

        var element = this.prefix.child(sufix).tryGet();

        if (!element.length)
            return;

        (visible ? this.shownButton : this.hidden).append(element.detach());
    }

    setupAutocomplete($txt : JQuery) {

        var handler : number;
        var auto = $txt.typeahead({
            source: (query, response) => {
                if (handler)
                    clearTimeout(handler);

                handler = setTimeout(() => {
                    this.autoCompleter.getResults(query)
                        .then(entities=> response(entities));
                }, 300);
            },

            sorter: items => items,
            matcher: item => true,
            highlighter: (item: Entities.EntityValue) => $("<div>").append(
                $("<span>")
                    .attr("data-type", item.runtimeInfo.type)
                    .attr("data-id", item.runtimeInfo.id)
                    .text(item.toStr)).html(),

            updater : (val: Entities.EntityValue) => this.onAutocompleteSelected(val)
        });
    }

    onAutocompleteSelected(entityValue: Entities.EntityValue) {
        throw new Error("onAutocompleteSelected is abstract");
    }

    getNiceName(typeName: string) : string {

        var t = this.options.types.filter(a=> a.name == typeName);

        return t.length ? t[0].niceName : typeName;
    }
}

export interface EntityAutocompleter {
    getResults(term: string): Promise<Entities.EntityValue[]>;
}

export interface AutocompleteResult {
    id: number;
    text: string;
    type: string;
    link: string;
}

export class AjaxEntityAutocompleter implements EntityAutocompleter {

    controllerUrl: string;

    getData: (term: string) => any;

    constructor(controllerUrl: string, getData: (term: string) => any) {
        this.controllerUrl = controllerUrl;
        this.getData = getData;
    }

    lastXhr: JQueryXHR; //To avoid previous requests results to be shown

    getResults(term: string): Promise<Entities.EntityValue[]> {
        if (this.lastXhr)
            this.lastXhr.abort();

        return new Promise<Entities.EntityValue[]>((resolve, failure) => {
            this.lastXhr = $.ajax({
                url: this.controllerUrl,
                data: this.getData(term),
                success: function (data: AutocompleteResult[]) {
                    this.lastXhr = null;
                    var entities = data.map(item=> new Entities.EntityValue(new Entities.RuntimeInfo(item.type, item.id, false), item.text, item.link));
                    resolve(entities);
                }
            });
        });
    }

}

export class EntityLine extends EntityBase {

    _create() {
        var $txt = this.prefix.child(Entities.Keys.toStr).tryGet().filter(".sf-entity-autocomplete");
        if ($txt.length) {
            this.autoCompleter = new AjaxEntityAutocompleter(this.options.autoCompleteUrl || SF.Urls.autocomplete,
                term => ({ types: this.options.types.map(t=> t.name).join(","), l: 5, q: term }));

            this.setupAutocomplete($txt);
        }
    }

    getLink(itemPrefix?: string): string {
        return this.prefix.child(Entities.Keys.link).get().attr("href");
    }

    getToString(itemPrefix?: string): string {
        return this.prefix.child(Entities.Keys.link).get().text();
    }

    setEntitySpecific(entityValue: Entities.EntityValue, itemPrefix?: string) {
        var link = this.prefix.child(Entities.Keys.link).get();
        link.text(entityValue == null ? null : entityValue.toStr);
        if (link.is('a'))
            link.attr('href', entityValue == null ? null : entityValue.link);
        this.prefix.child(Entities.Keys.toStr).get().val('');
        
        this.visible(this.prefix.child(Entities.Keys.link).tryGet(), entityValue != null);
        this.visible(this.prefix.get().find("ul.typeahead.dropdown-menu"), entityValue == null);
        this.visible(this.prefix.child(Entities.Keys.toStr).tryGet(), entityValue == null);
    }

    visible(element : JQuery, visible: boolean) {
        if (!element.length)
            return;

        if (visible)
            this.shownButton.before(element.detach());
        else
            this.hidden.append(element.detach());
    }

    onAutocompleteSelected(entityValue: Entities.EntityValue) {
        this.setEntity(entityValue);
    }
}

export class EntityCombo extends EntityBase {

    static key_combo = "sfCombo";

    combo() {
        return this.prefix.child(EntityCombo.key_combo).get();
    }

    setEntitySpecific(entityValue: Entities.EntityValue) {
        var c = this.combo();

        if (entityValue == null)
            c.val(null);
        else {
            var o = c.children("option[value='" + entityValue.runtimeInfo.key() + "']");
            if (o.length == 1)
                o.html(entityValue.toStr);
            else
                c.add($("<option value='{0}'/>".format(entityValue.runtimeInfo.key())).text(entityValue.toStr));

            c.val(entityValue.runtimeInfo.key());
        }
    }

    getToString(itemPrefix?: string): string {
        return this.combo().children("option[value='" + this.combo().val() + "']").text();
    }

    combo_selected() {
        var val = this.combo().val();

        var ri = Entities.RuntimeInfo.fromKey(val);

        this.setEntity(ri == null ? null : new Entities.EntityValue(ri, this.getToString()));
    }
}

export class EntityLineDetail extends EntityBase {

    options: EntityBaseOptions;

    constructor(element: JQuery, options: EntityBaseOptions) {
        super(element, options);
    }

    containerDiv(itemPrefix?: string) {
        return this.prefix.child("sfDetail").get();
    }

    fixInputGroup() {
    }

    setEntitySpecific(entityValue: Entities.EntityValue, itemPrefix?: string) {
        if (entityValue == null)
            return;

        if (!entityValue.isLoaded())
            throw new Error("EntityLineDetail requires a loaded Entities.EntityHtml, consider calling Navigator.loadPartialView");
    }

    onCreating(prefix: string): Promise<Entities.EntityValue> {
        if (this.creating != null)
            return this.creating(prefix);

        if (this.options.template)
            return Promise.resolve(this.getEmbeddedTemplate(prefix));

        return this.typeChooser(t=>t.creable).then(type=> {
            if (!type)
                return null;

            return type.preConstruct().then(args=> {
                if (!args)
                    return null;

                var newEntity = new Entities.EntityHtml(prefix, new Entities.RuntimeInfo(type.name, null, true), lang.signum.newEntity);

                return Navigator.requestPartialView(newEntity, this.defaultViewOptions(args));
            });
        });
    }

    find_click(): Promise<string> {
        return this.onFinding(this.options.prefix).then(result => {
            if (result == null) {
                this.notifyChanges(false);
                return null;
            }

            if (result.isLoaded())
                return Promise.resolve(<Entities.EntityHtml>result);

            return Navigator.requestPartialView(new Entities.EntityHtml(this.options.prefix, result.runtimeInfo), this.defaultViewOptions(null));
        }).then(result => {
                if (result) {
                    this.setEntity(result);
                    return this.options.prefix;
                }

                return null;
            });
    }
}



export interface EntityListBaseOptions extends EntityBaseOptions {
    maxElements?: number;
    remove?: boolean;
    reorder?: boolean;
}

export class EntityListBase extends EntityBase {
    static key_index = "sfIndex";
    static key_rowId = "sfRowId";

    options: EntityListBaseOptions;
    finding: (prefix: string) => Promise<Entities.EntityValue>;  // DEPRECATED!
    findingMany: (prefix: string) => Promise<Entities.EntityValue[]>;

    constructor(element: JQuery, options: EntityListBaseOptions) {
        super(element, options);
    }

    runtimeInfo(itemPrefix?: string): JQuery {
        return itemPrefix.child(Entities.Keys.runtimeInfo).get();
    }

    containerDiv(itemPrefix?: string): JQuery {
        var containerDivId = itemPrefix.child(EntityList.key_entity);

        var result = containerDivId.tryGet();

        if (result.length)
            return result;

        return SF.hiddenDiv(containerDivId, "").insertAfter(this.runtimeInfo(itemPrefix));
    }

    getEmbeddedTemplate(itemPrefix?: string) {
        if (!this.options.template)
            throw new Error("no template in " + this.options.prefix);

        var result = new Entities.EntityHtml(itemPrefix,
            new Entities.RuntimeInfo(this.singleType(), null, true), this.options.templateToString);

        var replaced = this.options.template.replace(new RegExp(this.options.prefix.child("0"), "gi"), itemPrefix)

        result.loadHtml(replaced);

        return result;
    }

    extractEntityHtml(itemPrefix?: string): Entities.EntityHtml {
        var runtimeInfo = Entities.RuntimeInfo.getFromPrefix(itemPrefix);

        var div = this.containerDiv(itemPrefix);

        var result = new Entities.EntityHtml(itemPrefix, runtimeInfo,
            this.getToString(itemPrefix),
            this.getLink(itemPrefix));

        result.html = div.children();

        div.html(null);

        return result;
    }

    setEntity(entityValue: Entities.EntityValue, itemPrefix?: string) {

        if (entityValue == null)
            throw new Error("entityValue is mandatory on setEntityItem");

        this.setEntitySpecific(entityValue, itemPrefix)

        entityValue.assertPrefixAndType(itemPrefix, this.options.types);

        if (entityValue.isLoaded())
            this.containerDiv(itemPrefix).html((<Entities.EntityHtml>entityValue).html);

        Entities.RuntimeInfo.setFromPrefix(itemPrefix, entityValue.runtimeInfo);

        this.updateButtonsDisplay();
        this.notifyChanges(true);
        if (!SF.isEmpty(this.entityChanged)) {
            this.entityChanged();
        }
    }

    create_click(): Promise<string> {
        var itemPrefix = this.reserveNextPrefix();
        return this.onCreating(itemPrefix).then(entity => {
            if (entity) {
                this.addEntity(entity, itemPrefix);
                return itemPrefix;
            }

            this.notifyChanges(false);
            return null;
        }).then(
            prefix => { this.freeReservedPrefix(itemPrefix); return prefix; },
            error => { this.freeReservedPrefix(itemPrefix); throw error; return ""; });
    }

    addEntitySpecific(entityValue: Entities.EntityValue, itemPrefix: string) {
        //virtual
    }

    addEntity(entityValue: Entities.EntityValue, itemPrefix: string) {
        if (entityValue == null)
            throw new Error("entityValue is mandatory on setEntityItem");

        this.addEntitySpecific(entityValue, itemPrefix);

        if (entityValue)
            entityValue.assertPrefixAndType(itemPrefix, this.options.types);

        if (entityValue.isLoaded())
            this.containerDiv(itemPrefix).html((<Entities.EntityHtml>entityValue).html);
        Entities.RuntimeInfo.setFromPrefix(itemPrefix, entityValue.runtimeInfo);

        this.updateButtonsDisplay();
        this.notifyChanges(true);
        if (!SF.isEmpty(this.entityChanged)) {
            this.entityChanged();
        }
    }

    removeEntitySpecific(itemPrefix: string) {
        //virtual
    }

    removeEntity(itemPrefix: string) {
        this.removeEntitySpecific(itemPrefix);

        this.updateButtonsDisplay();
        this.notifyChanges(true);
        if (!SF.isEmpty(this.entityChanged)) {
            this.entityChanged();
        }
    }

    itemSuffix(): string {
        throw new Error("itemSuffix is abstract");
    }

    getItems(): JQuery {
        throw new Error("getItems is abstract");
    }

    getPrefixes(): string[] {
        return this.getItems().toArray()
            .map((e: HTMLElement) => e.id.before("_" + this.itemSuffix()));
    }

    getRuntimeInfos(): Entities.RuntimeInfo[] {
        return this.getPrefixes().map(p=> Entities.RuntimeInfo.getFromPrefix(p));
    }

    reservedPrefixes: string[] = []; 

    reserveNextPrefix(): string {

        var currentPrefixes = this.getItems().toArray().map((e: HTMLElement) => e.id.before("_" + this.itemSuffix()));

        for (var i = 0; ; i++)
        {
            var newPrefix = this.options.prefix + "_" + i;

            if (this.reservedPrefixes.indexOf(newPrefix) == -1 &&
                currentPrefixes.indexOf(newPrefix) == -1) {

                this.reservedPrefixes.push(newPrefix);

                return newPrefix;
            }
        }
    }

    freeReservedPrefix(itemPrefix : string) {
        var index = this.reservedPrefixes.indexOf(itemPrefix);
        if (index == -1)
            throw Error("itemPrefix not reserved: " + itemPrefix);

        return this.reservedPrefixes.splice(index, 1);
    }

    getLastPosIndex(): number {
        var $last = this.getItems().filter(":last");
        if ($last.length == 0) {
            return -1;
        }

        var lastId = $last[0].id;
        var lastPrefix = lastId.substring(0, lastId.indexOf(this.itemSuffix()) - 1);

        return this.getPosIndex(lastPrefix);
    }

    getNextPosIndex(): string {
        return (this.getLastPosIndex() + 1).toString();
    }

    canAddItems() {
        return SF.isEmpty(this.options.maxElements) || this.getItems().length < this.options.maxElements;
    }

    find_click(): Promise<string> {

        var prefixes = [];

        return this.onFindingMany(this.options.prefix).then(result => {
            if (result) {

                result.forEach(ev=> {
                    var pr = this.reserveNextPrefix();
                    prefixes.push(pr);
                    this.addEntity(ev, pr);
                });

                return prefixes.join(",");
            }

            this.notifyChanges(false);
            return null;
        }).then(
            prefix => { prefixes.forEach(this.freeReservedPrefix); return prefix; },
            error => { prefixes.forEach(this.freeReservedPrefix); throw error; return ""; });
    }

    onFinding(prefix: string): Promise<Entities.EntityValue> {
        throw new Error("onFinding is deprecated in EntityListBase");
    }

    onFindingMany(prefix: string): Promise<Entities.EntityValue[]> {
        if (this.findingMany != null)
            return this.findingMany(prefix);

        return this.typeChooser(t => t.findable).then(type=> {
            if (type == null)
                return null;

            return Finder.findMany({
                webQueryName: type.name,
                prefix: prefix,
            });
        });
    }

    moveUp(itemPrefix: string) {

        var suffix = this.itemSuffix();
        var $item = itemPrefix.child(suffix).get();
        var $itemPrev = $item.prev();

        if ($itemPrev.length == 0) {
            return;
        }

        var itemPrevPrefix = $itemPrev[0].id.parent(suffix);

        var prevNewIndex = this.getPosIndex(itemPrevPrefix);
        this.setPosIndex(itemPrefix, prevNewIndex);
        this.setPosIndex(itemPrevPrefix, prevNewIndex + 1);

        $item.insertBefore($itemPrev);

        this.notifyChanges(true);
    }

    moveDown(itemPrefix: string) {

        var suffix = this.itemSuffix();
        var $item = itemPrefix.child(suffix).get();
        var $itemNext = $item.next();

        if ($itemNext.length == 0) {
            return;
        }

        var itemNextPrefix = $itemNext[0].id.parent(suffix);

        var nextNewIndex = this.getPosIndex(itemNextPrefix);
        this.setPosIndex(itemPrefix, nextNewIndex);
        this.setPosIndex(itemNextPrefix, nextNewIndex - 1);

        $item.insertAfter($itemNext);

        this.notifyChanges(true);
    }

    getPosIndex(itemPrefix: string) {
        return parseInt(itemPrefix.child(EntityListBase.key_index).get().val());
    }

    setPosIndex(itemPrefix: string, newIndex: number) {
        var $indexes = itemPrefix.child(EntityListBase.key_index).get().val(newIndex.toString());
    }
}

export class EntityList extends EntityListBase {

    static key_list = "sfList";

    _create() {
        var list = this.prefix.child(EntityList.key_list).get();

        list.change(() => this.selection_Changed());

        SF.onVisible(list).then(() => {
            if (list.height() < this.shownButton.height())
                list.css("min-height", this.shownButton.height());
        });

        this.selection_Changed();
    }

    selection_Changed() {
        this.updateButtonsDisplay();
    }

    itemSuffix() {
        return Entities.Keys.toStr;
    }

    updateLinks(newToStr: string, newLink: string, itemPrefix?: string) {
        itemPrefix.child(Entities.Keys.toStr).get().html(newToStr);
    }

    selectedItemPrefix(): string {
        var $items = this.getItems().filter(":selected");
        if ($items.length == 0) {
            return null;
        }

        var nameSelected = $items[0].id;
        return nameSelected.before("_" + this.itemSuffix());
    }

    getItems(): JQuery {
        return this.prefix.child(EntityList.key_list).get().children("option");
    }

    view_click(): Promise<string> {
        var selectedItemPrefix = this.selectedItemPrefix();

        var entityHtml = this.extractEntityHtml(selectedItemPrefix);

        return this.onViewing(entityHtml).then(result=> {
            if (result) {
                this.setEntity(result, selectedItemPrefix);
                return selectedItemPrefix;
            }
            else {
                this.setEntity(entityHtml, selectedItemPrefix); //previous entity passed by reference
                return null;
            }
        });
    }



    updateButtonsDisplay() {
        var canAdd = this.canAddItems();
        this.visibleButton("btnCreate", canAdd);
        this.visibleButton("btnFind", canAdd);

        var hasSelected = this.selectedItemPrefix() != null;
        this.visibleButton("btnView", hasSelected);
        this.visibleButton("btnRemove", hasSelected);
        this.visibleButton("btnUp", hasSelected);
        this.visibleButton("btnDown", hasSelected);

        this.fixInputGroup();
    }

    getToString(itemPrefix?: string): string {
        return itemPrefix.child(Entities.Keys.toStr).get().text();
    }

    setEntitySpecific(entityValue: Entities.EntityValue, itemPrefix?: string) {
        itemPrefix.child(Entities.Keys.toStr).get().text(entityValue.toStr);
    }

    addEntitySpecific(entityValue: Entities.EntityValue, itemPrefix: string) {

        this.inputGroup.before(SF.hiddenInput(itemPrefix.child(EntityList.key_index), this.getNextPosIndex()));
        this.inputGroup.before(SF.hiddenInput(itemPrefix.child(EntityList.key_rowId), ""));
        this.inputGroup.before(SF.hiddenInput(itemPrefix.child(Entities.Keys.runtimeInfo), entityValue.runtimeInfo.toString()));
        this.inputGroup.before(SF.hiddenDiv(itemPrefix.child(EntityList.key_entity), ""));

        var select = this.prefix.child(EntityList.key_list).get();
        select.children('option').attr('selected', false); //Fix for Firefox: Set selected after retrieving the html of the select

        var ri = entityValue.runtimeInfo;

        $("<option/>")
            .attr("id", itemPrefix.child(Entities.Keys.toStr))
            .attr("value", "")
            .attr('selected', true)
            .text(entityValue.toStr)
            .attr('title', this.options.isEmbedded ? null : (this.getNiceName(ri.type) + (ri.id ? " " + ri.id : null)))
            .appendTo(select);
    }

    remove_click(): Promise<string> {
        var selectedItemPrefix = this.selectedItemPrefix();
        return this.onRemove(selectedItemPrefix).then(result=> {
            if (result) {
                var next = this.getItems().filter(":selected").next();
                if (next.length == 0)
                    next = this.getItems().filter(":selected").prev();

                this.removeEntity(selectedItemPrefix);

                next.attr("selected", "selected");
                this.selection_Changed();

                return selectedItemPrefix;
            }

            return null;
        });
    }

    removeEntitySpecific(itemPrefix: string) {
        itemPrefix.child(Entities.Keys.runtimeInfo).get().remove();
        itemPrefix.child(Entities.Keys.toStr).get().remove();
        itemPrefix.child(EntityList.key_entity).tryGet().remove();
        itemPrefix.child(EntityList.key_index).tryGet().remove();
        itemPrefix.child(EntityList.key_rowId).tryGet().remove();
    }

    moveUp_click() {
        this.moveUp(this.selectedItemPrefix());
    }

    moveDown_click() {
        this.moveDown(this.selectedItemPrefix());
    }
}

export interface EntityListDetailOptions extends EntityListBaseOptions {
    detailDiv: string;
}

export class EntityListDetail extends EntityList {

    options: EntityListDetailOptions;

    selection_Changed() {
        super.selection_Changed();
        this.stageCurrentSelected();
    }

    remove_click(): Promise<string>  {
        return super.remove_click().then(result => { this.stageCurrentSelected(); return result })
    }

    create_click() {
        return super.create_click().then(result => { this.stageCurrentSelected(); return result; });
    }

    find_click() {
        return super.find_click().then(result => { this.stageCurrentSelected(); return result; })
    }

    stageCurrentSelected() {
        var selPrefix = this.selectedItemPrefix();

        var detailDiv = $("#" + this.options.detailDiv)

        var currentChildren = detailDiv.children();
        var currentPrefix = currentChildren.length ? currentChildren[0].id.parent(EntityListDetail.key_entity) : null;
        if (currentPrefix == selPrefix) {
            return;
        }

        var hideCurrent = () => {

            if (currentPrefix) {
                currentChildren.hide();
                this.runtimeInfo(currentPrefix).after(currentChildren);
            }
        }; 

        if (selPrefix) {
            var selContainer = this.containerDiv(selPrefix);

            var promise = selContainer.children().length ? Promise.resolve<void>(null) :
                Navigator.requestPartialView(new Entities.EntityHtml(selPrefix, Entities.RuntimeInfo.getFromPrefix(selPrefix), null, null), this.defaultViewOptions(null))
                    .then<void>(e=> selContainer.html(e.html));

            promise.then(() =>
            {
                detailDiv.append(selContainer);
                selContainer.show();

                if (currentPrefix) {
                    currentChildren.hide();
                    this.runtimeInfo(currentPrefix).after(currentChildren);
                }
            }); 
{'fraction_non_alphanumeric': 0.12604977507561452, 'fraction_numerical': 0.0011054304269725024, 'mean_word_length': 2.3202834276392923, 'pattern_counts': {'":': 8, '<': 93, '<?xml version=': 0, '>': 199, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2211801', 'n_tokens_mistral': 18247, 'n_tokens_neox': 17484, 'n_words': 3107}