You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1239 lines
40 KiB
1239 lines
40 KiB
/*******************************************************************************
|
|
|
|
µMatrix - a Chromium browser extension to black/white list requests.
|
|
Copyright (C) 2014 Raymond Hill
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see {http://www.gnu.org/licenses/}.
|
|
|
|
Home: https://github.com/gorhill/uMatrix
|
|
*/
|
|
|
|
/* global punycode, vAPI, uDom */
|
|
/* jshint esnext: true, bitwise: false */
|
|
|
|
/******************************************************************************/
|
|
/******************************************************************************/
|
|
|
|
(function() {
|
|
|
|
'use strict';
|
|
|
|
/******************************************************************************/
|
|
/******************************************************************************/
|
|
|
|
// Must be consistent with definitions in matrix.js
|
|
const Pale = 0x00;
|
|
const Dark = 0x80;
|
|
const Transparent = 0;
|
|
const Red = 1;
|
|
const Green = 2;
|
|
const Gray = 3;
|
|
const DarkRed = Dark | Red;
|
|
const PaleRed = Pale | Red;
|
|
const DarkGreen = Dark | Green;
|
|
const PaleGreen = Pale | Green;
|
|
const DarkGray = Dark | Gray;
|
|
const PaleGray = Pale | Gray;
|
|
|
|
var matrixSnapshot = {};
|
|
var groupsSnapshot = [];
|
|
var allHostnamesSnapshot = 'do not leave this initial string empty';
|
|
|
|
var targetTabId;
|
|
var matrixCellHotspots = null;
|
|
|
|
var matrixHeaderPrettyNames = {
|
|
'all': '',
|
|
'cookie': '',
|
|
'css': '',
|
|
'image': '',
|
|
'plugin': '',
|
|
'script': '',
|
|
'xhr': '',
|
|
'frame': '',
|
|
'other': ''
|
|
};
|
|
|
|
var firstPartyLabel = '';
|
|
var blacklistedHostnamesLabel = '';
|
|
|
|
/******************************************************************************/
|
|
/******************************************************************************/
|
|
|
|
// https://github.com/gorhill/httpswitchboard/issues/345
|
|
|
|
var onMessage = function(msg) {
|
|
if ( msg.what !== 'urlStatsChanged' ) {
|
|
return;
|
|
}
|
|
if ( matrixSnapshot.url !== msg.pageURL ) {
|
|
return;
|
|
}
|
|
queryMatrixSnapshot(makeMenu);
|
|
};
|
|
|
|
var messager = vAPI.messaging.channel('popup.js', onMessage);
|
|
|
|
/******************************************************************************/
|
|
/******************************************************************************/
|
|
|
|
function getUserSetting(setting) {
|
|
return matrixSnapshot.userSettings[setting];
|
|
}
|
|
|
|
function setUserSetting(setting, value) {
|
|
matrixSnapshot.userSettings[setting] = value;
|
|
messager.send({
|
|
what: 'userSettings',
|
|
name: setting,
|
|
value: value
|
|
});
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function updateMatrixSnapshot() {
|
|
var snapshotReady = function() {
|
|
updateMatrixColors();
|
|
updateMatrixBehavior();
|
|
updateMatrixButtons();
|
|
};
|
|
queryMatrixSnapshot(snapshotReady);
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
// For display purpose, create four distinct groups of rows:
|
|
// 0th: literal "1st-party" row
|
|
// 1st: page domain's related
|
|
// 2nd: whitelisted
|
|
// 3rd: graylisted
|
|
// 4th: blacklisted
|
|
|
|
function getGroupStats() {
|
|
|
|
// Try to not reshuffle groups around while popup is opened if
|
|
// no new hostname added.
|
|
var latestDomainListSnapshot = Object.keys(matrixSnapshot.rows).sort().join();
|
|
if ( latestDomainListSnapshot === allHostnamesSnapshot ) {
|
|
return groupsSnapshot;
|
|
}
|
|
allHostnamesSnapshot = latestDomainListSnapshot;
|
|
|
|
// First, group according to whether at least one node in the domain
|
|
// hierarchy is white or blacklisted
|
|
var pageDomain = matrixSnapshot.domain;
|
|
var rows = matrixSnapshot.rows;
|
|
var columnOffsets = matrixSnapshot.headers;
|
|
var anyTypeOffset = columnOffsets['*'];
|
|
var hostname, domain;
|
|
var row, color, count, groupIndex;
|
|
var domainToGroupMap = {};
|
|
|
|
// These have hard-coded position which cannot be overriden
|
|
domainToGroupMap['1st-party'] = 0;
|
|
domainToGroupMap[pageDomain] = 1;
|
|
|
|
// 1st pass: domain wins if it has an explicit rule or a count
|
|
for ( hostname in rows ) {
|
|
if ( rows.hasOwnProperty(hostname) === false ) {
|
|
continue;
|
|
}
|
|
if ( hostname === '*' || hostname === '1st-party' ) {
|
|
continue;
|
|
}
|
|
domain = rows[hostname].domain;
|
|
if ( domain === pageDomain || hostname !== domain ) {
|
|
continue;
|
|
}
|
|
row = rows[domain];
|
|
color = row.temporary[anyTypeOffset];
|
|
if ( color === DarkGreen ) {
|
|
domainToGroupMap[domain] = 2;
|
|
continue;
|
|
}
|
|
if ( color === DarkRed ) {
|
|
domainToGroupMap[domain] = 4;
|
|
continue;
|
|
}
|
|
count = row.counts[anyTypeOffset];
|
|
if ( count !== 0 ) {
|
|
domainToGroupMap[domain] = 3;
|
|
continue;
|
|
}
|
|
}
|
|
// 2nd pass: green wins
|
|
for ( hostname in rows ) {
|
|
if ( rows.hasOwnProperty(hostname) === false ) {
|
|
continue;
|
|
}
|
|
row = rows[hostname];
|
|
domain = row.domain;
|
|
if ( domainToGroupMap.hasOwnProperty(domain) ) {
|
|
continue;
|
|
}
|
|
color = row.temporary[anyTypeOffset];
|
|
if ( color === DarkGreen ) {
|
|
domainToGroupMap[domain] = 2;
|
|
}
|
|
}
|
|
// 3rd pass: gray with count wins
|
|
for ( hostname in rows ) {
|
|
if ( rows.hasOwnProperty(hostname) === false ) {
|
|
continue;
|
|
}
|
|
row = rows[hostname];
|
|
domain = row.domain;
|
|
if ( domainToGroupMap.hasOwnProperty(domain) ) {
|
|
continue;
|
|
}
|
|
color = row.temporary[anyTypeOffset];
|
|
count = row.counts[anyTypeOffset];
|
|
if ( color !== DarkRed && count !== 0 ) {
|
|
domainToGroupMap[domain] = 3;
|
|
}
|
|
}
|
|
// 4th pass: red wins whatever is left
|
|
for ( hostname in rows ) {
|
|
if ( rows.hasOwnProperty(hostname) === false ) {
|
|
continue;
|
|
}
|
|
row = rows[hostname];
|
|
domain = row.domain;
|
|
if ( domainToGroupMap.hasOwnProperty(domain) ) {
|
|
continue;
|
|
}
|
|
color = row.temporary[anyTypeOffset];
|
|
if ( color === DarkRed ) {
|
|
domainToGroupMap[domain] = 4;
|
|
}
|
|
}
|
|
// 5th pass: gray wins whatever is left
|
|
for ( hostname in rows ) {
|
|
if ( rows.hasOwnProperty(hostname) === false ) {
|
|
continue;
|
|
}
|
|
domain = rows[hostname].domain;
|
|
if ( domainToGroupMap.hasOwnProperty(domain) ) {
|
|
continue;
|
|
}
|
|
domainToGroupMap[domain] = 3;
|
|
}
|
|
|
|
// Last pass: put each domain in a group
|
|
var groups = [ {}, {}, {}, {}, {} ];
|
|
var group;
|
|
for ( hostname in rows ) {
|
|
if ( rows.hasOwnProperty(hostname) === false ) {
|
|
continue;
|
|
}
|
|
if ( hostname === '*' ) {
|
|
continue;
|
|
}
|
|
domain = rows[hostname].domain;
|
|
groupIndex = domainToGroupMap[domain];
|
|
group = groups[groupIndex];
|
|
if ( group.hasOwnProperty(domain) === false ) {
|
|
group[domain] = {};
|
|
}
|
|
group[domain][hostname] = true;
|
|
}
|
|
|
|
groupsSnapshot = groups;
|
|
|
|
return groups;
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
// helpers
|
|
|
|
function getTemporaryColor(hostname, type) {
|
|
return matrixSnapshot.rows[hostname].temporary[matrixSnapshot.headers[type]];
|
|
}
|
|
|
|
function getPermanentColor(hostname, type) {
|
|
return matrixSnapshot.rows[hostname].permanent[matrixSnapshot.headers[type]];
|
|
}
|
|
|
|
function getCellClass(hostname, type) {
|
|
return 't' + getTemporaryColor(hostname, type).toString(16) +
|
|
' p' + getPermanentColor(hostname, type).toString(16);
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
// This is required for when we update the matrix while it is open:
|
|
// the user might have collapsed/expanded one or more domains, and we don't
|
|
// want to lose all his hardwork.
|
|
|
|
function getCollapseState(domain) {
|
|
var states = getUserSetting('popupCollapseSpecificDomains');
|
|
if ( states !== undefined && states[domain] !== undefined ) {
|
|
return states[domain];
|
|
}
|
|
return getUserSetting('popupCollapseDomains');
|
|
}
|
|
|
|
function toggleCollapseState(elem) {
|
|
if ( elem.ancestors('#matHead.collapsible').length > 0 ) {
|
|
toggleMainCollapseState(elem);
|
|
} else {
|
|
toggleSpecificCollapseState(elem);
|
|
}
|
|
}
|
|
|
|
function toggleMainCollapseState(uelem) {
|
|
var matHead = uelem.ancestors('#matHead.collapsible').toggleClass('collapsed');
|
|
var collapsed = matHead.hasClass('collapsed');
|
|
uDom('#matList .matSection.collapsible').toggleClass('collapsed', collapsed);
|
|
setUserSetting('popupCollapseDomains', collapsed);
|
|
|
|
var specificCollapseStates = getUserSetting('popupCollapseSpecificDomains') || {};
|
|
var domains = Object.keys(specificCollapseStates);
|
|
var i = domains.length;
|
|
var domain;
|
|
while ( i-- ) {
|
|
domain = domains[i];
|
|
if ( specificCollapseStates[domain] === collapsed ) {
|
|
delete specificCollapseStates[domain];
|
|
}
|
|
}
|
|
setUserSetting('popupCollapseSpecificDomains', specificCollapseStates);
|
|
}
|
|
|
|
function toggleSpecificCollapseState(uelem) {
|
|
// Remember collapse state forever, but only if it is different
|
|
// from main collapse switch.
|
|
var section = uelem.ancestors('.matSection.collapsible').toggleClass('collapsed');
|
|
var domain = section.prop('domain');
|
|
var collapsed = section.hasClass('collapsed');
|
|
var mainCollapseState = getUserSetting('popupCollapseDomains');
|
|
var specificCollapseStates = getUserSetting('popupCollapseSpecificDomains') || {};
|
|
if ( collapsed !== mainCollapseState ) {
|
|
specificCollapseStates[domain] = collapsed;
|
|
setUserSetting('popupCollapseSpecificDomains', specificCollapseStates);
|
|
} else if ( specificCollapseStates[domain] !== undefined ) {
|
|
delete specificCollapseStates[domain];
|
|
setUserSetting('popupCollapseSpecificDomains', specificCollapseStates);
|
|
}
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
// Update color of matrix cells(s)
|
|
// Color changes when rules change
|
|
|
|
function updateMatrixColors() {
|
|
var cells = uDom('.matrix .matRow.rw > .matCell').removeClass();
|
|
var i = cells.length;
|
|
var cell;
|
|
while ( i-- ) {
|
|
cell = cells.nodeAt(i);
|
|
cell.className = 'matCell ' + getCellClass(cell.hostname, cell.reqType);
|
|
}
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
// Update behavior of matrix:
|
|
// - Whether a section is collapsible or not. It is collapsible if:
|
|
// - It has at least one subdomain AND
|
|
// - There is no explicit rule anywhere in the subdomain cells AND
|
|
// - It is not part of group 3 (blacklisted hostnames)
|
|
|
|
function updateMatrixBehavior() {
|
|
matrixList = matrixList || uDom('#matList');
|
|
var sections = matrixList.descendants('.matSection');
|
|
var i = sections.length;
|
|
var section, subdomainRows, j, subdomainRow;
|
|
while ( i-- ) {
|
|
section = sections.at(i);
|
|
subdomainRows = section.descendants('.l2:not(.g4)');
|
|
j = subdomainRows.length;
|
|
while ( j-- ) {
|
|
subdomainRow = subdomainRows.at(j);
|
|
subdomainRow.toggleClass('collapsible', subdomainRow.descendants('.t81,.t82').length === 0);
|
|
}
|
|
section.toggleClass('collapsible', subdomainRows.filter('.collapsible').length > 0);
|
|
}
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
// handle user interaction with filters
|
|
|
|
function getCellAction(hostname, type, leaning) {
|
|
var temporaryColor = getTemporaryColor(hostname, type);
|
|
var hue = temporaryColor & 0x03;
|
|
// Special case: root toggle only between two states
|
|
if ( type === '*' && hostname === '*' ) {
|
|
return hue === Green ? 'blacklistMatrixCell' : 'whitelistMatrixCell';
|
|
}
|
|
// When explicitly blocked/allowed, can only graylist
|
|
var saturation = temporaryColor & 0x80;
|
|
if ( saturation === Dark ) {
|
|
return 'graylistMatrixCell';
|
|
}
|
|
return leaning === 'whitelisting' ? 'whitelistMatrixCell' : 'blacklistMatrixCell';
|
|
}
|
|
|
|
function handleFilter(button, leaning) {
|
|
// our parent cell knows who we are
|
|
var cell = button.ancestors('div.matCell');
|
|
var type = cell.prop('reqType');
|
|
var desHostname = cell.prop('hostname');
|
|
// https://github.com/gorhill/uMatrix/issues/24
|
|
// No hostname can happen -- like with blacklist meta row
|
|
if ( desHostname === '' ) {
|
|
return;
|
|
}
|
|
var request = {
|
|
what: getCellAction(desHostname, type, leaning),
|
|
srcHostname: matrixSnapshot.scope,
|
|
desHostname: desHostname,
|
|
type: type
|
|
};
|
|
messager.send(request, updateMatrixSnapshot);
|
|
}
|
|
|
|
function handleWhitelistFilter(button) {
|
|
handleFilter(button, 'whitelisting');
|
|
}
|
|
|
|
function handleBlacklistFilter(button) {
|
|
handleFilter(button, 'blacklisting');
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
var matrixRowPool = [];
|
|
var matrixSectionPool = [];
|
|
var matrixGroupPool = [];
|
|
var matrixRowTemplate = null;
|
|
var matrixList = null;
|
|
|
|
var startMatrixUpdate = function() {
|
|
matrixList = matrixList || uDom('#matList');
|
|
matrixList.detach();
|
|
var rows = matrixList.descendants('.matRow');
|
|
rows.detach();
|
|
matrixRowPool = matrixRowPool.concat(rows.toArray());
|
|
var sections = matrixList.descendants('.matSection');
|
|
sections.detach();
|
|
matrixSectionPool = matrixSectionPool.concat(sections.toArray());
|
|
var groups = matrixList.descendants('.matGroup');
|
|
groups.detach();
|
|
matrixGroupPool = matrixGroupPool.concat(groups.toArray());
|
|
};
|
|
|
|
var endMatrixUpdate = function() {
|
|
// https://github.com/gorhill/httpswitchboard/issues/246
|
|
// If the matrix has no rows, we need to insert a dummy one, invisible,
|
|
// to ensure the extension pop-up is properly sized. This is needed because
|
|
// the header pane's `position` property is `fixed`, which means it doesn't
|
|
// affect layout size, hence the matrix header row will be truncated.
|
|
if ( matrixSnapshot.rowCount <= 1 ) {
|
|
matrixList.append(createMatrixRow().css('visibility', 'hidden'));
|
|
}
|
|
updateMatrixBehavior();
|
|
matrixList.css('display', '');
|
|
matrixList.appendTo('.paneContent');
|
|
};
|
|
|
|
var createMatrixGroup = function() {
|
|
var group = matrixGroupPool.pop();
|
|
if ( group ) {
|
|
return uDom(group).removeClass().addClass('matGroup');
|
|
}
|
|
return uDom('<div>').addClass('matGroup');
|
|
};
|
|
|
|
var createMatrixSection = function() {
|
|
var section = matrixSectionPool.pop();
|
|
if ( section ) {
|
|
return uDom(section).removeClass().addClass('matSection');
|
|
}
|
|
return uDom('<div>').addClass('matSection');
|
|
};
|
|
|
|
var createMatrixRow = function() {
|
|
var row = matrixRowPool.pop();
|
|
if ( row ) {
|
|
row.style.visibility = '';
|
|
row = uDom(row);
|
|
row.descendants('.matCell').removeClass().addClass('matCell');
|
|
row.removeClass().addClass('matRow');
|
|
return row;
|
|
}
|
|
if ( matrixRowTemplate === null ) {
|
|
matrixRowTemplate = uDom('#templates .matRow');
|
|
}
|
|
return matrixRowTemplate.clone();
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
function renderMatrixHeaderRow() {
|
|
var matHead = uDom('#matHead.collapsible');
|
|
matHead.toggleClass('collapsed', getUserSetting('popupCollapseDomains'));
|
|
var cells = matHead.descendants('.matCell');
|
|
cells.at(0)
|
|
.prop('reqType', '*')
|
|
.prop('hostname', '*')
|
|
.addClass(getCellClass('*', '*'));
|
|
cells.at(1)
|
|
.prop('reqType', 'cookie')
|
|
.prop('hostname', '*')
|
|
.addClass(getCellClass('*', 'cookie'));
|
|
cells.at(2)
|
|
.prop('reqType', 'css')
|
|
.prop('hostname', '*')
|
|
.addClass(getCellClass('*', 'css'));
|
|
cells.at(3)
|
|
.prop('reqType', 'image')
|
|
.prop('hostname', '*')
|
|
.addClass(getCellClass('*', 'image'));
|
|
cells.at(4)
|
|
.prop('reqType', 'plugin')
|
|
.prop('hostname', '*')
|
|
.addClass(getCellClass('*', 'plugin'));
|
|
cells.at(5)
|
|
.prop('reqType', 'script')
|
|
.prop('hostname', '*')
|
|
.addClass(getCellClass('*', 'script'));
|
|
cells.at(6)
|
|
.prop('reqType', 'xhr')
|
|
.prop('hostname', '*')
|
|
.addClass(getCellClass('*', 'xhr'));
|
|
cells.at(7)
|
|
.prop('reqType', 'frame')
|
|
.prop('hostname', '*')
|
|
.addClass(getCellClass('*', 'frame'));
|
|
cells.at(8)
|
|
.prop('reqType', 'other')
|
|
.prop('hostname', '*')
|
|
.addClass(getCellClass('*', 'other'));
|
|
uDom('#matHead .matRow').css('display', '');
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function renderMatrixCellDomain(cell, domain) {
|
|
var contents = cell.prop('reqType', '*')
|
|
.prop('hostname', domain)
|
|
.addClass(getCellClass(domain, '*'))
|
|
.contents();
|
|
contents.nodeAt(0).textContent = domain === '1st-party' ?
|
|
firstPartyLabel :
|
|
punycode.toUnicode(domain);
|
|
contents.nodeAt(1).textContent = ' ';
|
|
}
|
|
|
|
function renderMatrixCellSubdomain(cell, domain, subomain) {
|
|
var contents = cell.prop('reqType', '*')
|
|
.prop('hostname', subomain)
|
|
.addClass(getCellClass(subomain, '*'))
|
|
.contents();
|
|
contents.nodeAt(0).textContent = punycode.toUnicode(subomain.slice(0, subomain.lastIndexOf(domain)-1)) + '.';
|
|
contents.nodeAt(1).textContent = punycode.toUnicode(domain);
|
|
}
|
|
|
|
function renderMatrixMetaCellDomain(cell, domain) {
|
|
var contents = cell.prop('reqType', '*')
|
|
.prop('hostname', domain)
|
|
.addClass(getCellClass(domain, '*'))
|
|
.contents();
|
|
contents.nodeAt(0).textContent = '\u2217.' + punycode.toUnicode(domain);
|
|
contents.nodeAt(1).textContent = ' ';
|
|
}
|
|
|
|
function renderMatrixCellType(cell, hostname, type, count) {
|
|
cell.prop('reqType', type)
|
|
.prop('hostname', hostname)
|
|
.prop('count', count)
|
|
.addClass(getCellClass(hostname, type));
|
|
if ( count ) {
|
|
cell.text(count);
|
|
} else {
|
|
cell.text('\u00A0');
|
|
}
|
|
}
|
|
|
|
function renderMatrixCellTypes(cells, hostname, countName) {
|
|
var counts = matrixSnapshot.rows[hostname][countName];
|
|
var countIndices = matrixSnapshot.headers;
|
|
renderMatrixCellType(cells.at(1), hostname, 'cookie', counts[countIndices.cookie]);
|
|
renderMatrixCellType(cells.at(2), hostname, 'css', counts[countIndices.css]);
|
|
renderMatrixCellType(cells.at(3), hostname, 'image', counts[countIndices.image]);
|
|
renderMatrixCellType(cells.at(4), hostname, 'plugin', counts[countIndices.plugin]);
|
|
renderMatrixCellType(cells.at(5), hostname, 'script', counts[countIndices.script]);
|
|
renderMatrixCellType(cells.at(6), hostname, 'xhr', counts[countIndices.xhr]);
|
|
renderMatrixCellType(cells.at(7), hostname, 'frame', counts[countIndices.frame]);
|
|
renderMatrixCellType(cells.at(8), hostname, 'other', counts[countIndices.other]);
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function makeMatrixRowDomain(domain) {
|
|
var matrixRow = createMatrixRow().addClass('rw');
|
|
var cells = matrixRow.descendants('.matCell');
|
|
renderMatrixCellDomain(cells.at(0), domain);
|
|
renderMatrixCellTypes(cells, domain, 'counts');
|
|
return matrixRow;
|
|
}
|
|
|
|
function makeMatrixRowSubdomain(domain, subdomain) {
|
|
var matrixRow = createMatrixRow().addClass('rw');
|
|
var cells = matrixRow.descendants('.matCell');
|
|
renderMatrixCellSubdomain(cells.at(0), domain, subdomain);
|
|
renderMatrixCellTypes(cells, subdomain, 'counts');
|
|
return matrixRow;
|
|
}
|
|
|
|
function makeMatrixMetaRowDomain(domain) {
|
|
var matrixRow = createMatrixRow().addClass('rw');
|
|
var cells = matrixRow.descendants('.matCell');
|
|
renderMatrixMetaCellDomain(cells.at(0), domain);
|
|
renderMatrixCellTypes(cells, domain, 'totals');
|
|
return matrixRow;
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function renderMatrixMetaCellType(cell, count) {
|
|
// https://github.com/gorhill/uMatrix/issues/24
|
|
// Don't forget to reset cell properties
|
|
cell.addClass('t1')
|
|
.prop('reqType', '')
|
|
.prop('hostname', '')
|
|
.prop('count', count);
|
|
if ( count ) {
|
|
cell.text(count);
|
|
} else {
|
|
cell.text('\u00A0');
|
|
}
|
|
}
|
|
|
|
function makeMatrixMetaRow(totals) {
|
|
var typeOffsets = matrixSnapshot.headers;
|
|
var matrixRow = createMatrixRow().at(0).addClass('ro');
|
|
var cells = matrixRow.descendants('.matCell');
|
|
var contents = cells.at(0).addClass('t81').contents();
|
|
cells.at(0).prop('reqType', '*').prop('hostname', '');
|
|
contents.nodeAt(0).textContent = ' ';
|
|
contents.nodeAt(1).textContent = blacklistedHostnamesLabel.replace('{{count}}', totals[typeOffsets['*']]);
|
|
renderMatrixMetaCellType(cells.at(1), totals[typeOffsets.cookie]);
|
|
renderMatrixMetaCellType(cells.at(2), totals[typeOffsets.css]);
|
|
renderMatrixMetaCellType(cells.at(3), totals[typeOffsets.image]);
|
|
renderMatrixMetaCellType(cells.at(4), totals[typeOffsets.plugin]);
|
|
renderMatrixMetaCellType(cells.at(5), totals[typeOffsets.script]);
|
|
renderMatrixMetaCellType(cells.at(6), totals[typeOffsets.xhr]);
|
|
renderMatrixMetaCellType(cells.at(7), totals[typeOffsets.frame]);
|
|
renderMatrixMetaCellType(cells.at(8), totals[typeOffsets.other]);
|
|
return matrixRow;
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function computeMatrixGroupMetaStats(group) {
|
|
var headers = matrixSnapshot.headers;
|
|
var n = Object.keys(headers).length;
|
|
var totals = new Array(n);
|
|
var i = n;
|
|
while ( i-- ) {
|
|
totals[i] = 0;
|
|
}
|
|
var rows = matrixSnapshot.rows, row;
|
|
for ( var hostname in rows ) {
|
|
if ( rows.hasOwnProperty(hostname) === false ) {
|
|
continue;
|
|
}
|
|
row = rows[hostname];
|
|
if ( group.hasOwnProperty(row.domain) === false ) {
|
|
continue;
|
|
}
|
|
if ( row.counts[headers['*']] === 0 ) {
|
|
continue;
|
|
}
|
|
totals[0] += 1;
|
|
for ( i = 1; i < n; i++ ) {
|
|
totals[i] += row.counts[i];
|
|
}
|
|
}
|
|
return totals;
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
// Compare hostname helper, to order hostname in a logical manner:
|
|
// top-most < bottom-most, take into account whether IP address or
|
|
// named hostname
|
|
|
|
function hostnameCompare(a,b) {
|
|
// Normalize: most significant parts first
|
|
if ( !a.match(/^\d+(\.\d+){1,3}$/) ) {
|
|
var aa = a.split('.');
|
|
a = aa.slice(-2).concat(aa.slice(0,-2).reverse()).join('.');
|
|
}
|
|
if ( !b.match(/^\d+(\.\d+){1,3}$/) ) {
|
|
var bb = b.split('.');
|
|
b = bb.slice(-2).concat(bb.slice(0,-2).reverse()).join('.');
|
|
}
|
|
return a.localeCompare(b);
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function makeMatrixGroup0SectionDomain() {
|
|
return makeMatrixRowDomain('1st-party').addClass('g0 l1');
|
|
}
|
|
|
|
function makeMatrixGroup0Section() {
|
|
var domainDiv = createMatrixSection().prop('domain', '1st-party');
|
|
makeMatrixGroup0SectionDomain().appendTo(domainDiv);
|
|
return domainDiv;
|
|
}
|
|
|
|
function makeMatrixGroup0() {
|
|
// Show literal "1st-party" row only if there is
|
|
// at least one 1st-party hostname
|
|
if ( Object.keys(groupsSnapshot[1]).length === 0 ) {
|
|
return;
|
|
}
|
|
var groupDiv = createMatrixGroup().addClass('g0');
|
|
makeMatrixGroup0Section().appendTo(groupDiv);
|
|
groupDiv.appendTo(matrixList);
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function makeMatrixGroup1SectionDomain(domain) {
|
|
return makeMatrixRowDomain(domain)
|
|
.addClass('g1 l1');
|
|
}
|
|
|
|
function makeMatrixGroup1SectionSubomain(domain, subdomain) {
|
|
return makeMatrixRowSubdomain(domain, subdomain)
|
|
.addClass('g1 l2');
|
|
}
|
|
|
|
function makeMatrixGroup1SectionMetaDomain(domain) {
|
|
return makeMatrixMetaRowDomain(domain).addClass('g1 l1 meta');
|
|
}
|
|
|
|
function makeMatrixGroup1Section(hostnames) {
|
|
var domain = hostnames[0];
|
|
var domainDiv = createMatrixSection()
|
|
.toggleClass('collapsed', getCollapseState(domain))
|
|
.prop('domain', domain);
|
|
if ( hostnames.length > 1 ) {
|
|
makeMatrixGroup1SectionMetaDomain(domain)
|
|
.appendTo(domainDiv);
|
|
}
|
|
makeMatrixGroup1SectionDomain(domain)
|
|
.appendTo(domainDiv);
|
|
for ( var i = 1; i < hostnames.length; i++ ) {
|
|
makeMatrixGroup1SectionSubomain(domain, hostnames[i])
|
|
.appendTo(domainDiv);
|
|
}
|
|
return domainDiv;
|
|
}
|
|
|
|
function makeMatrixGroup1(group) {
|
|
var domains = Object.keys(group).sort(hostnameCompare);
|
|
if ( domains.length ) {
|
|
var groupDiv = createMatrixGroup().addClass('g1');
|
|
makeMatrixGroup1Section(Object.keys(group[domains[0]]).sort(hostnameCompare))
|
|
.appendTo(groupDiv);
|
|
for ( var i = 1; i < domains.length; i++ ) {
|
|
makeMatrixGroup1Section(Object.keys(group[domains[i]]).sort(hostnameCompare))
|
|
.appendTo(groupDiv);
|
|
}
|
|
groupDiv.appendTo(matrixList);
|
|
}
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function makeMatrixGroup2SectionDomain(domain) {
|
|
return makeMatrixRowDomain(domain)
|
|
.addClass('g2 l1');
|
|
}
|
|
|
|
function makeMatrixGroup2SectionSubomain(domain, subdomain) {
|
|
return makeMatrixRowSubdomain(domain, subdomain)
|
|
.addClass('g2 l2');
|
|
}
|
|
|
|
function makeMatrixGroup2SectionMetaDomain(domain) {
|
|
return makeMatrixMetaRowDomain(domain).addClass('g2 l1 meta');
|
|
}
|
|
|
|
function makeMatrixGroup2Section(hostnames) {
|
|
var domain = hostnames[0];
|
|
var domainDiv = createMatrixSection()
|
|
.toggleClass('collapsed', getCollapseState(domain))
|
|
.prop('domain', domain);
|
|
if ( hostnames.length > 1 ) {
|
|
makeMatrixGroup2SectionMetaDomain(domain).appendTo(domainDiv);
|
|
}
|
|
makeMatrixGroup2SectionDomain(domain)
|
|
.appendTo(domainDiv);
|
|
for ( var i = 1; i < hostnames.length; i++ ) {
|
|
makeMatrixGroup2SectionSubomain(domain, hostnames[i])
|
|
.appendTo(domainDiv);
|
|
}
|
|
return domainDiv;
|
|
}
|
|
|
|
function makeMatrixGroup2(group) {
|
|
var domains = Object.keys(group).sort(hostnameCompare);
|
|
if ( domains.length) {
|
|
var groupDiv = createMatrixGroup()
|
|
.addClass('g2');
|
|
makeMatrixGroup2Section(Object.keys(group[domains[0]]).sort(hostnameCompare))
|
|
.appendTo(groupDiv);
|
|
for ( var i = 1; i < domains.length; i++ ) {
|
|
makeMatrixGroup2Section(Object.keys(group[domains[i]]).sort(hostnameCompare))
|
|
.appendTo(groupDiv);
|
|
}
|
|
groupDiv.appendTo(matrixList);
|
|
}
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function makeMatrixGroup3SectionDomain(domain) {
|
|
return makeMatrixRowDomain(domain)
|
|
.addClass('g3 l1');
|
|
}
|
|
|
|
function makeMatrixGroup3SectionSubomain(domain, subdomain) {
|
|
return makeMatrixRowSubdomain(domain, subdomain)
|
|
.addClass('g3 l2');
|
|
}
|
|
|
|
function makeMatrixGroup3SectionMetaDomain(domain) {
|
|
return makeMatrixMetaRowDomain(domain).addClass('g3 l1 meta');
|
|
}
|
|
|
|
function makeMatrixGroup3Section(hostnames) {
|
|
var domain = hostnames[0];
|
|
var domainDiv = createMatrixSection()
|
|
.toggleClass('collapsed', getCollapseState(domain))
|
|
.prop('domain', domain);
|
|
if ( hostnames.length > 1 ) {
|
|
makeMatrixGroup3SectionMetaDomain(domain).appendTo(domainDiv);
|
|
}
|
|
makeMatrixGroup3SectionDomain(domain)
|
|
.appendTo(domainDiv);
|
|
for ( var i = 1; i < hostnames.length; i++ ) {
|
|
makeMatrixGroup3SectionSubomain(domain, hostnames[i])
|
|
.appendTo(domainDiv);
|
|
}
|
|
return domainDiv;
|
|
}
|
|
|
|
function makeMatrixGroup3(group) {
|
|
var domains = Object.keys(group).sort(hostnameCompare);
|
|
if ( domains.length) {
|
|
var groupDiv = createMatrixGroup()
|
|
.addClass('g3');
|
|
makeMatrixGroup3Section(Object.keys(group[domains[0]]).sort(hostnameCompare))
|
|
.appendTo(groupDiv);
|
|
for ( var i = 1; i < domains.length; i++ ) {
|
|
makeMatrixGroup3Section(Object.keys(group[domains[i]]).sort(hostnameCompare))
|
|
.appendTo(groupDiv);
|
|
}
|
|
groupDiv.appendTo(matrixList);
|
|
}
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function makeMatrixGroup4SectionDomain(domain) {
|
|
return makeMatrixRowDomain(domain)
|
|
.addClass('g4 l1');
|
|
}
|
|
|
|
function makeMatrixGroup4SectionSubomain(domain, subdomain) {
|
|
return makeMatrixRowSubdomain(domain, subdomain)
|
|
.addClass('g4 l2');
|
|
}
|
|
|
|
function makeMatrixGroup4Section(hostnames) {
|
|
var domain = hostnames[0];
|
|
var domainDiv = createMatrixSection()
|
|
.prop('domain', domain);
|
|
makeMatrixGroup4SectionDomain(domain)
|
|
.appendTo(domainDiv);
|
|
for ( var i = 1; i < hostnames.length; i++ ) {
|
|
makeMatrixGroup4SectionSubomain(domain, hostnames[i])
|
|
.appendTo(domainDiv);
|
|
}
|
|
return domainDiv;
|
|
}
|
|
|
|
function makeMatrixGroup4(group) {
|
|
var domains = Object.keys(group).sort(hostnameCompare);
|
|
if ( domains.length === 0 ) {
|
|
return;
|
|
}
|
|
var groupDiv = createMatrixGroup().addClass('g4');
|
|
createMatrixSection()
|
|
.addClass('g4Meta')
|
|
.toggleClass('g4Collapsed', !!getUserSetting('popupHideBlacklisted'))
|
|
.appendTo(groupDiv);
|
|
makeMatrixMetaRow(computeMatrixGroupMetaStats(group), 'g4')
|
|
.appendTo(groupDiv);
|
|
makeMatrixGroup4Section(Object.keys(group[domains[0]]).sort(hostnameCompare))
|
|
.appendTo(groupDiv);
|
|
for ( var i = 1; i < domains.length; i++ ) {
|
|
makeMatrixGroup4Section(Object.keys(group[domains[i]]).sort(hostnameCompare))
|
|
.appendTo(groupDiv);
|
|
}
|
|
groupDiv.appendTo(matrixList);
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
var makeMenu = function() {
|
|
var groupStats = getGroupStats();
|
|
|
|
if ( Object.keys(groupStats).length === 0 ) {
|
|
return;
|
|
}
|
|
|
|
// https://github.com/gorhill/httpswitchboard/issues/31
|
|
if ( matrixCellHotspots ) {
|
|
matrixCellHotspots.detach();
|
|
}
|
|
|
|
renderMatrixHeaderRow();
|
|
|
|
startMatrixUpdate();
|
|
makeMatrixGroup0(groupStats[0]);
|
|
makeMatrixGroup1(groupStats[1]);
|
|
makeMatrixGroup2(groupStats[2]);
|
|
makeMatrixGroup3(groupStats[3]);
|
|
makeMatrixGroup4(groupStats[4]);
|
|
endMatrixUpdate();
|
|
|
|
initScopeCell();
|
|
updateMatrixButtons();
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// Do all the stuff that needs to be done before building menu et al.
|
|
|
|
function initMenuEnvironment() {
|
|
uDom('body').css('font-size', getUserSetting('displayTextSize'));
|
|
uDom('body').toggleClass('colorblind', getUserSetting('colorBlindFriendly') === true);
|
|
|
|
var prettyNames = matrixHeaderPrettyNames;
|
|
var keys = Object.keys(prettyNames);
|
|
var i = keys.length;
|
|
var cell, key, text;
|
|
while ( i-- ) {
|
|
key = keys[i];
|
|
cell = uDom('#matHead .matCell[data-req-type="'+ key +'"]');
|
|
text = vAPI.i18n(key + 'PrettyName');
|
|
cell.text(text);
|
|
prettyNames[key] = text;
|
|
}
|
|
|
|
firstPartyLabel = uDom('[data-i18n="matrix1stPartyLabel"]').text();
|
|
blacklistedHostnamesLabel = uDom('[data-i18n="matrixBlacklistedHostnames"]').text();
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
// Create page scopes for the web page
|
|
|
|
function selectGlobalScope() {
|
|
setUserSetting('popupScopeLevel', '*');
|
|
updateMatrixSnapshot();
|
|
dropDownMenuHide();
|
|
}
|
|
|
|
function selectDomainScope() {
|
|
setUserSetting('popupScopeLevel', 'domain');
|
|
updateMatrixSnapshot();
|
|
dropDownMenuHide();
|
|
}
|
|
|
|
function selectSiteScope() {
|
|
setUserSetting('popupScopeLevel', 'site');
|
|
updateMatrixSnapshot();
|
|
dropDownMenuHide();
|
|
}
|
|
|
|
function getClassFromScope() {
|
|
if ( matrixSnapshot.scope === '*' ) {
|
|
return 'tScopeGlobal';
|
|
}
|
|
if ( matrixSnapshot.scope === matrixSnapshot.domain ) {
|
|
return 'tScopeDomain';
|
|
}
|
|
return 'tScopeSite';
|
|
}
|
|
|
|
function initScopeCell() {
|
|
// It's possible there is no page URL at this point: some pages cannot
|
|
// be filtered by µMatrix.
|
|
if ( matrixSnapshot.url === '' ) {
|
|
return;
|
|
}
|
|
// Fill in the scope menu entries
|
|
if ( matrixSnapshot.hostname === matrixSnapshot.domain ) {
|
|
uDom('#scopeKeySite').css('display', 'none');
|
|
} else {
|
|
uDom('#scopeKeySite').text(matrixSnapshot.hostname);
|
|
}
|
|
uDom('#scopeKeyDomain').text(matrixSnapshot.domain);
|
|
updateScopeCell();
|
|
}
|
|
|
|
function updateScopeCell() {
|
|
uDom('body')
|
|
.removeClass('tScopeGlobal tScopeDomain tScopeSite')
|
|
.addClass(getClassFromScope());
|
|
uDom('#scopeCell').text(matrixSnapshot.scope.replace('*', '\u2217'));
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function updateMatrixSwitches() {
|
|
var switches = matrixSnapshot.tSwitches;
|
|
for ( var switchName in switches ) {
|
|
if ( switches.hasOwnProperty(switchName) === false ) {
|
|
continue;
|
|
}
|
|
uDom('#mtxSwitch_' + switchName).toggleClass('switchTrue', switches[switchName]);
|
|
|
|
}
|
|
var count = matrixSnapshot.blockedCount;
|
|
var button = uDom('#mtxSwitch_matrix-off');
|
|
button.descendants('span.badge').text(count.toLocaleString());
|
|
button.attr('data-tip', button.attr('data-tip').replace('{{count}}', count));
|
|
uDom('body').toggleClass('powerOff', switches['matrix-off']);
|
|
}
|
|
|
|
function toggleMatrixSwitch() {
|
|
var pos = this.id.indexOf('_');
|
|
if ( pos === -1 ) {
|
|
return;
|
|
}
|
|
var switchName = this.id.slice(pos + 1);
|
|
var request = {
|
|
what: 'toggleMatrixSwitch',
|
|
switchName: switchName,
|
|
srcHostname: matrixSnapshot.scope
|
|
};
|
|
messager.send(request, updateMatrixSnapshot);
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function updatePersistButton() {
|
|
var diffCount = matrixSnapshot.diff.length;
|
|
var button = uDom('#buttonPersist');
|
|
button.contents()
|
|
.filter(function(){return this.nodeType===3;})
|
|
.first()
|
|
.text(diffCount > 0 ? '\uf13e' : '\uf023');
|
|
button.descendants('span.badge').text(diffCount > 0 ? diffCount : '');
|
|
var disabled = diffCount === 0;
|
|
button.toggleClass('disabled', disabled);
|
|
uDom('#buttonRevertScope').toggleClass('disabled', disabled);
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function persistMatrix() {
|
|
var request = {
|
|
what: 'applyDiffToPermanentMatrix',
|
|
diff: matrixSnapshot.diff
|
|
};
|
|
messager.send(request, updateMatrixSnapshot);
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
// rhill 2014-03-12: revert completely ALL changes related to the
|
|
// current page, including scopes.
|
|
|
|
function revertMatrix() {
|
|
var request = {
|
|
what: 'applyDiffToTemporaryMatrix',
|
|
diff: matrixSnapshot.diff
|
|
};
|
|
messager.send(request, updateMatrixSnapshot);
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
// Buttons which are affected by any changes in the matrix
|
|
|
|
function updateMatrixButtons() {
|
|
updateScopeCell();
|
|
updateMatrixSwitches();
|
|
updatePersistButton();
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function revertAll() {
|
|
var request = {
|
|
what: 'revertTemporaryMatrix'
|
|
};
|
|
messager.send(request, updateMatrixSnapshot);
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function buttonReloadHandler() {
|
|
messager.send({
|
|
what: 'forceReloadTab',
|
|
tabId: targetTabId
|
|
});
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function mouseenterMatrixCellHandler() {
|
|
matrixCellHotspots.appendTo(this);
|
|
}
|
|
|
|
function mouseleaveMatrixCellHandler() {
|
|
matrixCellHotspots.detach();
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function gotoExtensionURL() {
|
|
var url = uDom(this).attr('data-extension-url');
|
|
if ( url ) {
|
|
messager.send({ what: 'gotoExtensionURL', url: url });
|
|
}
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function gotoExternalURL() {
|
|
var url = this.getAttribute('data-external-url');
|
|
if ( url ) {
|
|
messager.send({ what: 'gotoURL', url: url });
|
|
}
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
function dropDownMenuShow() {
|
|
uDom(this).next('.dropdown-menu').addClass('show');
|
|
}
|
|
|
|
function dropDownMenuHide() {
|
|
uDom('.dropdown-menu').removeClass('show');
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
var onMatrixSnapshotReady = function(response) {
|
|
// Now that tabId and pageURL are set, we can build our menu
|
|
initMenuEnvironment();
|
|
makeMenu();
|
|
|
|
// After popup menu is built, check whether there is a non-empty matrix
|
|
if ( matrixSnapshot.url === '' ) {
|
|
uDom('#matHead').remove();
|
|
uDom('#toolbarLeft').remove();
|
|
|
|
// https://github.com/gorhill/httpswitchboard/issues/191
|
|
uDom('#noNetTrafficPrompt').text(vAPI.i18n('matrixNoNetTrafficPrompt'));
|
|
uDom('#noNetTrafficPrompt').css('display', '');
|
|
}
|
|
};
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
var queryMatrixSnapshot = function(callback) {
|
|
var request = {
|
|
what: 'matrixSnapshot',
|
|
tabId: targetTabId,
|
|
tabURL: matrixSnapshot.url
|
|
};
|
|
var snapshotReceived = function(response) {
|
|
matrixSnapshot = response;
|
|
callback();
|
|
};
|
|
messager.send(request, snapshotReceived);
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// Make menu only when popup html is fully loaded
|
|
|
|
uDom.onLoad(function() {
|
|
queryMatrixSnapshot(onMatrixSnapshotReady);
|
|
|
|
// Below is UI stuff which is not key to make the menu, so this can
|
|
// be done without having to wait for a tab to be bound to the menu.
|
|
|
|
// We reuse for all cells the one and only cell hotspots.
|
|
uDom('#whitelist').on('click', function() {
|
|
handleWhitelistFilter(uDom(this));
|
|
return false;
|
|
});
|
|
uDom('#blacklist').on('click', function() {
|
|
handleBlacklistFilter(uDom(this));
|
|
return false;
|
|
});
|
|
uDom('#domainOnly').on('click', function() {
|
|
toggleCollapseState(uDom(this));
|
|
return false;
|
|
});
|
|
matrixCellHotspots = uDom('#cellHotspots').detach();
|
|
uDom('body')
|
|
.on('mouseenter', '.matCell', mouseenterMatrixCellHandler)
|
|
.on('mouseleave', '.matCell', mouseleaveMatrixCellHandler);
|
|
uDom('#scopeKeyGlobal').on('click', selectGlobalScope);
|
|
uDom('#scopeKeyDomain').on('click', selectDomainScope);
|
|
uDom('#scopeKeySite').on('click', selectSiteScope);
|
|
uDom('[id^="mtxSwitch_"]').on('click', toggleMatrixSwitch);
|
|
uDom('#buttonPersist').on('click', persistMatrix);
|
|
uDom('#buttonRevertScope').on('click', revertMatrix);
|
|
|
|
uDom('#buttonRevertAll').on('click', revertAll);
|
|
uDom('#buttonReload').on('click', buttonReloadHandler);
|
|
uDom('.extensionURL').on('click', gotoExtensionURL);
|
|
uDom('.externalURL').on('click', gotoExternalURL);
|
|
|
|
uDom('body').on('click', '.dropdown-menu-button', dropDownMenuShow);
|
|
uDom('body').on('click', '.dropdown-menu-capture', dropDownMenuHide);
|
|
|
|
uDom('#matList').on('click', '.g4Meta', function() {
|
|
var collapsed = uDom(this)
|
|
.toggleClass('g4Collapsed')
|
|
.hasClass('g4Collapsed');
|
|
setUserSetting('popupHideBlacklisted', collapsed);
|
|
});
|
|
});
|
|
|
|
/******************************************************************************/
|
|
|
|
})();
|