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({