bucket: getBucket()

in src/StrippetsVisual.ts [457:707]


                            bucket: getBucket(buckets[i]),
                        };

                        populateUncertaintyFields(entity, entityIds, buckets, i);
                        highlightEntityAndMapIcon(entity, entityClass, entityColor, isHighlighted);
                        strippetsData[id].entities.push(entity);
                    }
                }
            }

            // Set highlighted state only if strippets contains a highlighted entity.
            if (!strippetsData[id].isHighlighted && isHighlighted) {
                strippetsData[id].isHighlighted = isHighlighted;
            }
        });
        const items = Object.keys(strippetsData).reduce((memo, key) => {
            memo.push(strippetsData[key]);
            return memo;
        }, []).sort((a, b) => {
            return a.order - b.order;
        });

        const bucketList = _.sortBy(bucketMap, (bucket: Bucket) => bucket.key);
        const numBuckets: number = Math.max(1, bucketList.length);
        bucketList.map(function (bucket: Bucket, index) {
            bucket.value = index / numBuckets;
        });

        return {
            items: items,
            iconMap: updateIM ? Object.keys(iconMap).map(key => {
                return iconMap[key];
            }) : [],
            highlights: isHighlightingOn ? {
                entities: Object.keys(highlightedEntities).reduce((memo, key) => {
                    memo.push(highlightedEntities[key]);
                    return memo;
                }, []),
                itemIds: items.reduce((memo, item) => {
                    if (item.isHighlighted) {
                        memo.push(item.id);
                    }
                    return memo;
                }, []),
            } : null,
        };
    }

    /**
     * Initializes an instance of the IVisual.
     *
     * @param {VisualConstructorOptions} options Initialization options for the visual.
     */
    constructor(options: VisualConstructorOptions) {
        const template = require('./../templates/strippets.handlebars');
        this.$loaderElement = $(require('./../templates/loader.handlebars')());
        this.element = $('<div/>');
        this.element.append(template());
        $(options.element).append(this.element);

        this.$container = this.element.find('.strippets-container');
        this.$tabs = this.element.find('.nav');
        this.selectionManager = options.host.createSelectionManager();
        this.host = this.selectionManager['hostServices'];
        this.colors = options.host.colors || (options.host['colorPalette'] ? options.host['colorPalette'].colors : []);

        this.inSandbox = this.element.parents('body.visual-sandbox').length > 0;

        this.viewportSize = { width: this.$container.parent().width(), height: this.$container.parent().height() };
        this.$container.width(this.viewportSize.width - this.$tabs.width());
        this.minOutlineCount = this.viewportSize.width / OUTLINE_WIDTH + 10;
        this.outlines = { $elem: this.$container.find('.outlines-panel') };
        this.thumbnails = { $elem: this.$container.find('.thumbnails-panel') };

        this.initializeTabs(this.$tabs);

        this.resizeOutlines = _.debounce(function () {
            if (this.outlines && this.outlines.instance) {
                this.outlines.instance.resize();
            }
            else if (this.thumbnails && this.thumbnails.instance) {
                this.thumbnails.instance.resize();
            }
        }, ENTITIES_REPOSITION_DELAY).bind(this);

        // Kill touch events to prevent PBI mobile app refreshing while scrolling strippets
        const killEvent = (event) => {
            event.originalEvent.stopPropagation();
            event.originalEvent.stopImmediatePropagation();
            return true;
        };
        this.$container.on('touchstart', killEvent);
        this.$container.on('touchmove', killEvent);
        this.$container.on('touchend', killEvent);

        const findApi = (methodName) => {
            return options.host[methodName] ? (arg) => {
                options.host[methodName](arg);
            } : this.host && this.host[methodName] ? (arg) => {
                this.host[methodName](arg);
            } : null;
        };

        this.loadMoreData = findApi("loadMoreData");
        this.launchUrl = findApi("launchUrl");
    }

    /**
     * Instantiates and configures the Outlines component
     * @returns {*|exports|module.exports}
     */
    private initializeOutlines(): any {
        const t = this;
        const Outlines = require('@uncharted/strippets');
        const $outlines = t.outlines.$elem;

        const outlinesInstance = new Outlines($outlines[0], {
            outline: {
                reader: {
                    enabled: true,
                    onLoadUrl: $.proxy(t.onLoadArticle, t),
                    onReaderOpened: (id) => {
                        t.lastOpenedStoryId = id;
                    },
                    onReaderClosed: () => {
                        t.lastOpenedStoryId = null;
                    },
                    onSourceUrlClicked: (href) => {
                        t.launchUrl && t.launchUrl(href);
                    },
                },
                enableExpandedMode: false,
            },
            autoGenerateIconMap: false,
            supportKeyboardNavigation: false,
            entityIcons: [],
        }, t.mediator);
        // set up infinite scroll
        let infiniteScrollTimeoutId: any;
        outlinesInstance.$viewport.on('scroll', (e) => {
            if ($(e.target).width() + e.target.scrollLeft >= e.target.scrollWidth) {
                infiniteScrollTimeoutId = setTimeout(() => {
                    clearTimeout(infiniteScrollTimeoutId);
                    if (!t.isLoadingMore && t.hasMoreData && t.loadMoreData) {
                        t.isLoadingMore = true;
                        t.showLoader();
                        t.loadMoreData();
                    }
                }, t.INFINITE_SCROLL_DELAY);
            }
        });

        // Register Click Event
        outlinesInstance.$viewport.off('click');

        return outlinesInstance;
    }

    // https://stackoverflow.com/questions/35962586/javascript-remove-inline-event-handlers-attributes-of-a-node#35962814
    public static removeScriptAttributes(el) {
        const attributes = [].slice.call(el.attributes);

        for (let i = 0; i < attributes.length; i++) {
            const att = attributes[i].name;

            if (att.indexOf('on') === 0) {
                el.attributes.removeNamedItem(att);
            }
        }
    }

    /**
     * Removes dangerous tags, such as scripts, from the given HTML content.
     * @param {String} html - HTML content to clean
     * @param {Array} whiteList - Array of HTML tag names to accept
     * @returns {String} HTML content, devoid of any tags not in the whitelist
     */
    public static sanitizeHTML(html: string, whiteList: string[]): string {
        let cleanHTML = '';
        if (html && whiteList && whiteList.length) {
            // Stack Overflow is all like NEVER PARSE HTML WITH REGEX
            // http://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags/1732454#1732454
            // plus the C# whitelist regex I found didn't work in JS
            // http://stackoverflow.com/questions/307013/how-do-i-filter-all-html-tags-except-a-certain-whitelist#315851
            // So going with the innerHTML approach...
            // http://stackoverflow.com/questions/6659351/removing-all-script-tags-from-html-with-js-regular-expression

            let doomedNodeList = [];

            if (!document.createTreeWalker) {
                return ''; // in case someone's hax0ring us?
            }
            let div = $('<div/>');

            // For the aforementioned reasons, we do need innerHTML, so suppress tslint
            // tslint:disable-next-line
            div.html(html);

            let filter: any = function (node) {
                if (whiteList.indexOf(node.nodeName.toUpperCase()) === -1) {
                    StrippetBrowser16424341054522.removeScriptAttributes(node);
                    return NodeFilter.FILTER_ACCEPT;
                }

                return NodeFilter.FILTER_SKIP;
            };

            filter.acceptNode = filter;

            // Create a tree walker (hierarchical iterator) that only exposes non-whitelisted nodes, which we'll delete.
            let treeWalker = document.createTreeWalker(
                div.get()[0],
                NodeFilter.SHOW_ELEMENT,
                filter,
                false
            );

            while (treeWalker.nextNode()) {
                doomedNodeList.push(treeWalker.currentNode);
            }

            let length = doomedNodeList.length;
            for (let i = 0; i < length; i++) {
                if (doomedNodeList[i].parentNode) {
                    try {
                        doomedNodeList[i].parentNode.removeChild(doomedNodeList[i]);
                    } catch (ex) { }
                }
            }

            // convert back to a string.
            cleanHTML = div.html().trim();
        }

        return cleanHTML;
    }

    /**
     * Handler for the readers to call when an article is ready to load.
     * If the article content is a readability URL, the actual article text will first be fetched.
     * The article is cleaned and highlighted before being returned in a Promise, as part of a reader config object.
     * @param {String} articleId - primary key value for the datum containing the article to load.
     */
    private onLoadArticle(articleId: string): any {
        const t = this;
        const data = _.find(<any>t.data.items, (d: any) => d.id === articleId);
        if (data) {
            if (StrippetBrowser16424341054522.isUrl(data.content)) {
                if (t.settings.content.readerContentType === 'readability') {
                    return new Promise((resolve: any, reject: any) => {
                        $.ajax({