lightbeam/data/list.js

622 строки
23 KiB
JavaScript

'use strict';
// List Visualization
// Display data in tabular format
(function(visualizations){
var list = new Emitter();
var breadcrumbStack = [];
visualizations.list = list;
list.name = "list";
list.on("init", onInit);
// list.on("connection", onConnection);
list.on("remove", onRemove);
list.on("showFilteredTable", function(filter){
showFilteredTable(filter);
});
list.on('reset', onReset);
function onReset(){
onRemove();
aggregate.emit('load', allConnections);
}
function onInit(){
// console.log('list::onInit()');
vizcanvas.classList.add("hide"); // we don't need vizcanvas here, so hide it
// A D3 visualization has a two main components, data-shaping, and setting up the D3 callbacks
// This binds our data to the D3 visualization and sets up the callbacks
initList();
initializeHandlers();
toggleShowHideHiddenButton();
aggregate.on('update', onUpdate);
}
function onUpdate(){
let { nodes } = aggregate;
let oldNodeRows = getAllRows().map(function(row) row.getAttribute('data-name'));
let newNodes = nodes.filter(function(node) {
return oldNodeRows.indexOf(node.name) < 0;
});
if (newNodes.length <= 0) {
return;
}
document.getElementById('refresh-data-link').textContent = 'Click here to refresh list...';
document.getElementById('refresh-data-row').classList.add('show');
return;
}
function onConnection(conn){
var connection = aggregate.connectionAsObject(conn);
}
function onRemove(){
// console.log('removing list');
// var startTime = Date.now();
resetCanvas();
aggregate.off('update', onUpdate);
// console.log('It took %s ms to remove list view', Date.now() - startTime);
}
function initList(){
var stage = document.querySelector('.stage');
// breadcrumb
initBreadcrumb();
// add number of row selected label
var selectedLabel = elem("div", {"class": "rows-selected-label blue-text"},[
elem("div", {"class": "some-selected hidden"}, [
elem("span", {"class": "num-selected"}),
" out of ",
elem("span", {"class": "num-total"}),
" sites selected"
]),
elem("div", {"class": "none-selected"}, [
elem("span", {"class": "num-total"}),
" sites"
])
]);
stage.appendChild(selectedLabel);
// list header
var table = elem("div", {'class': 'list-table'}, [
elem('table', {'role': 'grid', 'aria-label': 'Entering List table'}, [
elem('tr', {'class': 'refresh', 'id': 'refresh-data-row'}, [
elem('td', {'colspan': '7', 'id': 'refresh-data-link'})
]),
elem('thead', {'class': 'header-table'}, [
elem('tr', {'role':'row', 'tabIndex': '0'}, [
elem('th', elem('input', {'class': 'selected-header', type: 'checkbox', 'tabIndex': '-1'})),
elem('th', {'role':'gridcell'}, 'Type'),
elem('th', {'role':'gridcell'}, 'Prefs'),
elem('th', {'role':'gridcell'}, 'Website'),
elem('th', {'role':'gridcell'}, 'First Access'),
elem('th', {'role':'gridcell'}, 'Last Access'),
elem('th', {'class': 'sort-numeric', 'role': 'gridcell'}, 'Sites Connected')
])
]),
]),
elem('div', {'class': 'body-table'},
elem('table', {'role': 'grid'},
elem('tbody', {'class': 'list-body'})
)
)
]);
stage.appendChild(table);
showFilteredTable(); // showing all data so no filter param is passed here
updateBreadcrumb();
}
function initBreadcrumb(){
var stage = document.querySelector('.stage');
var breadcrumb = elem("div", {"class": "breadcrumb"});
stage.appendChild(breadcrumb);
}
function updateBreadcrumb(url){
// push to breadcrumbStack
breadcrumbStack.push(url ? url : "All Sites");
// remove all child nodes in breadcrumb container before we start mapping breadcrumbs to UI again
resetVisibleBreadcrumb();
// map breadcrumbs to UI
mapBreadcrumbsToUI();
}
var breadcrumbClickHandler = function(event){
var url = event.target.getAttribute("site-url");
var idxInStack = event.target.getAttribute("idx");
while ( breadcrumbStack.length > idxInStack ){
breadcrumbStack.pop();
}
showFilteredTable(url);
};
function mapBreadcrumbsToUI(){
var breadcrumb = document.querySelector(".breadcrumb");
var lastIdxInStack = breadcrumbStack.length-1;
// add "All Sites" to breadcrumb container
breadcrumb.appendChild( elem("div", {"class": "breadcrumb-chunk"}, breadcrumbStack[0]) );
// other than "All Sites", there is only 1 tier in breadcrumbStack
// add that tier to breadcrumb container
if ( lastIdxInStack == 1 ){
breadcrumb.appendChild( elem("div", {"class": "arrow-left"}) );
breadcrumb.appendChild( elem( "div",
{
"class": "breadcrumb-chunk no-click",
"site-url": breadcrumbStack[lastIdxInStack]
},
breadcrumbStack[lastIdxInStack]) );
}
// other than "All Sites", there are more than 1 tier in breadcrumbStack
// we only want to show "All Sites" and the last 2 tiers
// so add the last 2 tiers to breadcrumb container
if ( lastIdxInStack >= 2 ){
// second last tier
breadcrumb.appendChild( elem( "div", {"class": "arrow-left"}) );
breadcrumb.appendChild( elem( "div",
{
"class": "breadcrumb-chunk",
"site-url": breadcrumbStack[lastIdxInStack-1],
"idx": (lastIdxInStack-1)
},
breadcrumbStack[lastIdxInStack-1]) );
// last tier
breadcrumb.appendChild( elem("div", {"class": "arrow-left"}) );
breadcrumb.appendChild( elem( "div",
{
"class": "breadcrumb-chunk no-click",
"site-url": breadcrumbStack[lastIdxInStack],
"idx": lastIdxInStack
},
breadcrumbStack[lastIdxInStack]) );
}
// add breadcrumbs click event handler
var allBreadcrumbChunks = document.querySelectorAll(".breadcrumb-chunk");
toArray(allBreadcrumbChunks).forEach(function(chunk){
if ( !chunk.classList.contains("no-click") ){
chunk.addEventListener("click", breadcrumbClickHandler, false);
}
});
}
function resetVisibleBreadcrumb(){
var breadcrumbContainer = document.querySelector(".breadcrumb");
while ( breadcrumbContainer.firstChild ){
breadcrumbContainer.removeChild(breadcrumbContainer.firstChild);
}
}
function updateNumTotalRowsLabel(){
var numTotal = getAllRows().length;
var labels = document.querySelectorAll(".num-total");
for ( var i=0; i<labels.length; i++){
labels[i].textContent = numTotal;
}
}
function updateRowSelectedLabel(){
var numSelected = getSelectedRows().length;
var selectedLabel = document.querySelector(".some-selected");
var noneSelectedLabel = document.querySelector(".none-selected");
if ( numSelected > 0 ){
selectedLabel.querySelector(".num-selected").textContent = numSelected;
selectedLabel.classList.remove("hidden");
noneSelectedLabel.classList.add("hidden");
}else{
selectedLabel.classList.add("hidden");
noneSelectedLabel.classList.remove("hidden");
}
}
var lastFilter = null;
function showFilteredTable(filter){
if ( lastFilter != filter ) updateBreadcrumb(filter);
lastFilter = filter;
// remove existing table tbodys, if any
var table = document.querySelector(".list-table");
var tbody = table.querySelector('.list-body');
var tbodyParent = tbody.parentElement;
tbodyParent.removeChild(tbody);
var nodes = getNodes(filter);
tbodyParent.appendChild( createBody(nodes) );
resort(table);
// update other UI elements
document.querySelector('.selected-header').checked = false;
updateNumTotalRowsLabel();
updateRowSelectedLabel();
}
function getNodes(filter){
if( !filter ){ // if no filter, show all
return aggregate.getAllNodes();
}else{
var nodeMap = aggregate.nodeForKey(filter);
return Object.keys(nodeMap).map(function(key){ return nodeMap[key]; });
}
}
// A Node has the following properties:
// contentTypes: []
// cookieCount: #
// firstAccess: Date
// howMany: #
// method: []
// name: ""
// nodeType: site | thirdparty | both
// secureCount: #
// status: []
// subdomain: []
// visitedCount: #
function nodeToRow(node){
var settings = userSettings[node.name] || (node.nodeType == 'blocked' ? 'block' : '');
var iconUrl = node.nodeType === 'blocked'? 'icons/lightbeam_icon_empty_list.png' : 'icons/lightbeam_icon_list.png';
var listIcon = elem('img', {'src': iconUrl, 'class': node.nodeType === 'blocked'? 'no-update' :'update-table', 'role': 'gridcell'});
var row = elem('tr', {
'class': 'node ' + node.nodeType,
'data-pref': settings,
'data-name': node.name,
'site-url': node.name,
'role': 'row',
'tabIndex': '0'
}, [
elem('td', elem('input', {'type': 'checkbox', 'class': 'selected-row', 'tabIndex':'-1'})),
elem('td', {'data-sort-key': node.nodeType, 'role': 'gridcell'}, node.nodeType === 'thirdparty' ? 'Third Party' : (node.nodeType === 'blocked' ? 'Unknown' : 'Visited')),
elem('td', {'class': 'preferences', 'data-sort-key': settings, 'role': 'gridcell'}, '\u00A0'),
elem('td', {'data-sort-key': node.name, 'role': 'gridcell'}, [
listIcon,
node.name
]),
elem('td', {'data-sort-key': node.firstAccess, 'role': 'gridcell'}, (node.nodeType === 'blocked' ? 'Unknown' : formattedDate(node.firstAccess))),
elem('td', {'data-sort-key': node.lastAccess, 'role': 'gridcell'}, (node.nodeType === 'blocked' ? 'Unknown' : formattedDate(node.lastAccess))),
elem('td', {'data-sort-key': aggregate.getConnectionCount(node), 'role': 'gridcell'}, aggregate.getConnectionCount(node) + '')
]);
if (node.nodeType !== 'blocked'){
listIcon.addEventListener("mouseenter",tooltip.addTooltip);
listIcon.addEventListener("mouseleave",tooltip.hide);
row.addEventListener("mouseenter",function(){
row.childNodes[3].firstChild.setAttribute("src", "image/lightbeam_icon_list_blue.png");
});
row.addEventListener("mouseleave",function(){
row.childNodes[3].firstChild.setAttribute("src", iconUrl);
});
}
if (node.nodeType === 'blocked'){
row.dataset.isBlocked = true;
}
return row;
}
function createBody(nodes){
return elem("tbody", {
'class': 'list-body'
}, nodes.map(nodeToRow));
}
function sort(item1, item2){
if (item1[0] < item2[0]) return -1;
if (item2[0] < item1[0]) return 1;
return 0;
}
function reverseSort(item1, item2){
if (item1[0] < item2[0]) return 1;
if (item2[0] < item1[0]) return -1;
return 0;
}
function sortTableOnColumn(table, n){
return function(evt){ // we could probably determine the column from the event.target
// if this is sorted column, reverse
// if this is reversed column, re-sort
// if this is not sorted column, unset sorted flag on that column
var reversed = evt.target.classList.contains('reverse-sorted');
var sorted = evt.target.classList.contains('sorted');
if (!(sorted || reversed)){
var oldcolumn = table.querySelector('.sorted, .reverse-sorted');
if (oldcolumn){
oldcolumn.classList.remove('sorted');
oldcolumn.classList.remove('reverse-sorted');
}
}
var tbody = table.querySelector('tbody');
var rows = Array.prototype.slice.call(tbody.querySelectorAll('tr')).map(function(row){
if (evt.target.classList.contains('sort-numeric')){
return [parseInt(row.children[n].dataset.sortKey, 10), row];
}else{
return [row.children[n].dataset.sortKey, row];
}
});
if (sorted){
localStorage.lastSortColumn = n;
localStorage.lastSortDirection = 'reversed';
evt.target.classList.remove('sorted');
evt.target.classList.add('reverse-sorted');
rows.sort(reverseSort);
}else{
localStorage.lastSortColumn = n;
localStorage.lastSortDirection = 'forward';
evt.target.classList.remove('reverse-sorted');
evt.target.classList.add('sorted');
rows.sort(sort);
}
var frag = document.createDocumentFragment();
var preFrag = document.createDocumentFragment();
// Is this the preference column?
var prefCol = localStorage.lastSortColumn === '2';
rows.forEach(function(row){
var rowElement = row[1];
// Check if there are any preferences set for this row
var prefVal = rowElement.attributes.getNamedItem('data-pref').value;
if (prefCol && prefVal != ''){
// This row is marked with a preference and should
// be appended to the top fragment.
preFrag.appendChild(rowElement);
}else{
frag.appendChild(rowElement);
}
});
tbody.appendChild(preFrag);
tbody.appendChild(frag);
}
}
function resort(table){
var direction = localStorage.lastSortDirection;
if (direction){
var index = parseInt(localStorage.lastSortColumn, 10) + 1; // nth child is 1-based
var header = table.querySelector('th:nth-child(' + index + ')');
// set the opposite class on header, then click it to get the right sorting
header.classList.remove(direction === 'forward' ? 'sorted' : 'reverse-sorted');
header.classList.add(direction === 'forward' ? 'reverse-sorted' : 'sorted');
header.dispatchEvent(new MouseEvent('click'))
}
}
function resetCanvas(){
var listTable = document.querySelector('.stage .list-table');
if (listTable){
listTable.parentElement.removeChild(listTable);
}
var breadcrumb = document.querySelector('.stage .breadcrumb');
if (breadcrumb){
breadcrumb.parentElement.removeChild(breadcrumb);
}
breadcrumbStack = [];
var selectedLabel = document.querySelector(".rows-selected-label");
if (selectedLabel){
selectedLabel.parentElement.removeChild(selectedLabel);
}
document.querySelector('.stage-stack').removeEventListener('click', listStageStackClickHandler, false);
vizcanvas.classList.remove("hide");
}
function getAllRows() {
return Array.slice(document.querySelectorAll('.body-table tr'));
}
function getSelectedRows(){
// returns selected rows as an Array
return getAllRows().filter(function(item){
return item.querySelector('.selected-row:checked');
})
}
// Event handlers
function setUserSetting(row, pref) {
var site = row.dataset.name;
// change setting
userSettings[site] = pref;
// send change through to add-on
addon.emit('updateBlocklist', site, pref === 'block');
// modify row
row.dataset.pref = pref;
// Add sort order to preference column
row.querySelector('.preferences').dataset.sortKey = pref;
// Re-sort if sorted by preference
if(localStorage.lastSortColumn === '2'){
resort(document.querySelector(".list-table"));
}
// uncheck the row
row.querySelector('[type=checkbox]').checked = false;
row.classList.remove("checked");
}
// selectAllRows should only select VISIBLE rows
function selectAllRows(flag){
// apply flag to ALL rows first
var rows = document.querySelectorAll(".body-table tr");
for (var i = 0; i < rows.length; i++){
rows[i].querySelector(".selected-row").checked = flag;
highlightRow(rows[i],flag);
}
// and then exclude all the hidden rows
if ( document.querySelector(".hide-hidden-rows") ){
var hiddenRows = document.querySelectorAll(".list-table .body-table tr[data-pref=hide]");
for (var i = 0; i < hiddenRows.length; i++){
hiddenRows[i].querySelector(".selected-row").checked = false; // makes sure the hidden rows are always unchecked
highlightRow(hiddenRows[i],false);
}
}
togglePrefButtons();
}
function setPreferences(pref){
getSelectedRows().forEach(function(row){
setUserSetting(row, pref);
});
document.querySelector('.selected-header').checked = false;
updateRowSelectedLabel();
togglePrefButtons();
toggleShowHideHiddenButton();
}
function toggleHiddenSites(target){
if (target.dataset.state === 'shown'){
target.dataset.state = 'hidden';
target.textContent = 'Show Hidden';
document.querySelector('.stage-stack').classList.add('hide-hidden-rows');
localStorage.listViewHideRows = true;
}else{
target.dataset.state = 'shown';
target.textContent = 'Hide Hidden';
document.querySelector('.stage-stack').classList.remove('hide-hidden-rows');
localStorage.listViewHideRows = false;
}
}
// Restore state on load
if (localStorage.listViewHideRows){
var button = document.querySelector('.toggle-hidden a');
button.dataset.state = 'hidden';
button.textContent = 'Show Hidden';
document.querySelector('.stage-stack').classList.add('hide-hidden-rows');
}
var listStageStackClickHandler = function(event){
var target = event.target;
if(target.mozMatchesSelector('label[for=block-pref], label[for=block-pref] *') ){
confirmBlockSitesDialog(function(confirmed){
if ( confirmed ){
setPreferences('block');
}
});
}else if (target.mozMatchesSelector('label[for=hide-pref], label[for=hide-pref] *') ){
if ( doNotShowDialog(dialogNames.hideSites) ){
setPreferences('hide');
}else{
confirmHideSitesDialog(function(confirmed){
if ( confirmed ){
setPreferences('hide');
}
});
}
}else if (target.mozMatchesSelector('label[for=watch-pref], label[for=watch-pref] *')){
setPreferences('watch');
}else if(target.mozMatchesSelector('label[for=no-pref], label[for=no-pref] *')){
setPreferences('');
}else if(target.mozMatchesSelector('.toggle-hidden a')){
toggleHiddenSites(target);
}
};
// Install handlers
function initializeHandlers(){
try{
document.querySelector('.selected-header').addEventListener('change', function(event){
selectAllRows(event.target.checked);
}, false);
document.querySelector('.list-footer').querySelector(".legend-toggle").addEventListener("click", function(event){
toggleLegendSection(event.target,document.querySelector('.list-footer'));
});
document.querySelector('.stage-stack').addEventListener('click', listStageStackClickHandler, false);
// Add handler for rows
document.querySelector('.list-table').addEventListener('click', function(event){
var url = event.target.parentNode.dataset.sortKey;
var node = event.target;
if (node.mozMatchesSelector('td:first-child [type=checkbox]')){
while(node.mozMatchesSelector('.node *')){
node = node.parentElement;
}
highlightRow(node,node.querySelector("[type=checkbox]").checked);
togglePrefButtons();
}else if (node.mozMatchesSelector('.update-table') && url ){
showFilteredTable(url);
}
},false);
// Add handler to refresh rows
var refreshRow = document.querySelector("#refresh-data-row");
refreshRow.addEventListener('click', function onClick() {
var wereSelected, selected;
refreshRow.classList.remove('show');
// update the table
// what were selected should stay selected after the table has been updated
wereSelected = getSelectedRows().map(function(row){ return row.dataset.name; });
showFilteredTable(lastFilter);
selected = getAllRows().filter(function(row){ return wereSelected.indexOf(row.dataset.name) > -1 })
.map(function(rowToSelect){
rowToSelect.querySelector("[type=checkbox]").checked = true;
highlightRow(rowToSelect,true);
return;
});
}, false);
// Set sort handlers. nth-child(n+2) skips the checkbox column
var table = document.querySelector(".list-table");
var headers = Array.prototype.slice.call(table.querySelectorAll('th:nth-child(n+2)'));
headers.forEach(function(th, idx){
// idx+1 gives the actual column (skipping the checkbox the other way)
th.addEventListener('click', sortTableOnColumn(table, idx+1), false);
});
}catch(e){
console.log('Error: %o', e);
}
}
function highlightRow(node,rowChecked){
if (rowChecked){
node.classList.add("checked");
}else{
node.classList.remove("checked");
}
updateRowSelectedLabel();
}
function togglePrefButtons(){
var numChecked = document.querySelectorAll(".list-table .body-table tr input[type=checkbox]:checked").length;
var toggleOn = numChecked > 0;
var classToAdd = toggleOn ? "active" : "disabled";
var classToRemove = toggleOn ? "disabled" : "active";
// toggle on class
toArray(document.querySelectorAll("input[name=pref-options] + label")).forEach(function(option){
option.classList.add(classToAdd);
});
// toggle off class
toArray(document.querySelectorAll("input[name=pref-options] + label")).forEach(function(option){
option.classList.remove(classToRemove);
});
}
function toggleShowHideHiddenButton(){
if ( document.querySelectorAll("[data-pref='hide']").length > 0 ){
document.querySelector(".toggle-hidden").classList.remove("disabled");
}else{
document.querySelector(".toggle-hidden").classList.add("disabled");
}
}
})(visualizations);