There are 36 previous versions of this script.
the source is over 100KB, syntax highlighting in the browser is too slow
// ==UserScript==
// @name Google Search Filter Plus
// @description Filters google search results
// @namespace smk
// @include http://www.google.tld/
// @include http://www.google.tld/search?*
// @include http://www.google.tld/cse?*
// @include http://www.google.tld/custom?*
// @include https://www.google.com/
// @include https://www.google.com/search?*
// @include https://encrypted.google.com/
// @include https://encrypted.google.com/search?*
// @license MPL 1.1; http://www.mozilla.org/MPL/MPL-1.1.html
// ==/UserScript===
/**
history:
v1.19.6 (07-12-11):
[fix] filter link position
v1.19.5 (22-07-11):
[rem] removed old google prefLink positioning
v1.19.4 (16-07-11):
[fix] filter link position
v1.19.3 (20-06-11):
[fix] filter link position
v1.19.2 (12-05-11):
[fix] wiki entries
v1.19.1 (23-04-11):
[fix] google book results processing
v1.19 (13-04-11):
[chg] discard 'www' in addFilter dialog
v1.18 (13-04-11):
[add] google ssl filtering
[fix] video filter link
v1.17 (22-03-11):
[fix] pref subfilter already exists bug (a$$b$$c => a$$b$$d)
[fix] cancelling pref retains the edited filter list
[fix] adding empty filter raises exception
v1.16 (19-03-11):
[chg] using css selectors instead of xpath to increase selector speed
v1.15 (19-03-11):
[add] google instant ext
[fix] small fixes
v1.14 (16-03-11):
[fix] google custom search ext directLink
[fix] fixed & improved googlemonkeyr ext
v1.13 (15-03-11):
[fix] google custom search url matching
[add] prefLink in google custom search
v1.12 (15-03-11):
[fix] can't filter links more than twice when using the filter link
[add] removes google custom search link redirects in ext
v1.11 (15-03-11):
[add] added google custom search extension
v1.10 (15-03-11):
[add] supports title, summary filtering
[fix] various minor fixes
v1.04 (04-03-11):
[fix] whitelist hitcount bug
v1.03 (03-03-11):
[fix] pref window bug, add filter then edit filter exit will display 'filter already exists'
[fix] bug when importing filters, then selecting will select old value (before importing)
v1.02 (28-02-11):
[fix] google maps bug
[fix] live results googlemonkeyr autoload ext
[chg] hide parent node if all child nodes hidden, when resHidden==true
v1.01 (28-02-11):
[fix] image linkArea bug
[fix] live results bug
[chg] filterClass improvements, remNode improvements
v1.01 (27-02-11):
[add] googlemonkeyr pagerize support
v1.00 (19-02-11):
[add] new gui
[add] syntax highlighting
[add] syntax checking (uses abp)
[chg] uses adblock plus match engine for matching
v0.07 (04-11-09):
[fix] some minor bugs in _at_include, still need to improve speed after long time
v0.06 (19-01-09):
[fix] some problems regarding different search results (news items, etc.)
[fix] filter starting/ending with space problems (in filter editor);
v0.05 (18/01/09):
[fix] 'summary' option error
v0.04 (17-01-09):
[add] made filtering faster (by buffering nodes)
[add] added processing of exceptions
[add] 'optimize' function in filter composer
[chg] 'block' string to 'filter' (thanks to ekbookworldwide)
[add] whenever block a new site, show filter composer
[add] 'search box' filter: searchstr
v0.03 (08-01-09):
[fix] initial filters problem
v0.02 (04-01-09):
[fix] title not showing original filter
v0.01:
[chg] many changes to original script
*/
/**
thanks to:
adblock plus
cho45
ekbookworldwide
marti
sizzlemctwizzle
webismymind
*/
let config={
//blocked sites (default)
filters: [
'||daniweb.com',
'||velocityreviews.com',
],
//completely hide filter results (default)
resHidden: false,
//enabled extensions
ext: ['gmonkeyr','customSearch'],
//log component times
logTime: true,
}
/**
helper functions
*/
function $X(x, t, r) {
if (t && t.tagName)
var h = r, r = t, t = h;
var d = r ? r.ownerDocument || r : r = document, p;
switch (t) {
case XPathResult.NUMBER_TYPE:
p = 'numberValue';
break;
case XPathResult.STRING_TYPE:
p = 'stringValue';
break;
case XPathResult.BOOLEAN_TYPE:
p = 'booleanValue';
break;
case XPathResult.ANY_UNORDERED_NODE_TYPE:
case XPathResult.FIRST_ORDERED_NODE_TYPE:
p = 'singleNodeValue';
break;
default:
return d.evaluate(x, r, null, t || XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
}
return d.evaluate(x, r, null, t, null)[p];
}
/**
helper classes
*/
let logTime={
currTime: null,
start: function(){
logTime.currTime=new Date().getTime();
},
snap: function(msg){
GM_log(msg+': '+(new Date().getTime()-logTime.currTime)+'ms');
},
end: function(msg){
logTime.snap(msg);
logTime.currTime=null;
},
restart: function(msg){
logTime.snap(msg);
logTime.start();
},
profile: function(parent,funcName){
let tTime=0;
let func=parent[funcName];
parent[funcName]=function(){
let currTime=new Date().getTime();
let ret=func.apply(this,arguments);
tTime+=new Date().getTime()-currTime;
return ret;
};
this.snap=function(msg){
GM_log(msg+': '+tTime+'ms');
};
this.end=function(msg){
this.snap(msg);
tTime=0;
};
},
};
if(!config.logTime){
for(let key in logTime){
if(logTime[key].constructor==Function){
logTime[key]=function(){};
}
}
}
/**
adblock plus matching classes
*/
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Adblock Plus.
*
* The Initial Developer of the Original Code is
* Wladimir Palant.
* Portions created by the Initial Developer are Copyright (C) 2006-2010
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
*
* ***** END LICENSE BLOCK ***** */
/**
* @fileOverview Definition of Filter class and its subclasses.
*/
var EXPORTED_SYMBOLS = ["Filter", "InvalidFilter", "CommentFilter", "ActiveFilter", "RegExpFilter", "BlockingFilter", "WhitelistFilter", "ElemHideFilter"];
/**
* Abstract base class for filters
*
* @param {String} text string representation of the filter
* @constructor
*/
function Filter(text)
{
this.text = text;
this.subscriptions = [];
}
Filter.prototype =
{
/**
* String representation of the filter
* @type String
*/
text: null,
/**
* Filter subscriptions the filter belongs to
* @type Array of Subscription
*/
subscriptions: null,
/**
* Serializes the filter to an array of strings for writing out on the disk.
* @param {Array of String} buffer buffer to push the serialization results into
*/
serialize: function(buffer)
{
buffer.push("[Filter]");
buffer.push("text=" + this.text);
},
toString: function()
{
return this.text;
}
};
/**
* Cache for known filters, maps string representation to filter objects.
* @type Object
*/
Filter.knownFilters = {__proto__: null};
/**
* Regular expression that element hiding filters should match
* @type RegExp
*/
Filter.elemhideRegExp = /^([^\/\*\|\@"!]*?)#(?:([\w\-]+|\*)((?:\([\w\-]+(?:[$^*]?=[^\(\)"]*)?\))*)|#([^{}]+))$/;
/**
* Regular expression that RegExp filters specified as RegExps should match
* @type RegExp
*/
Filter.regexpRegExp = /^(@@)?\/.*\/(?:\$~?[\w\-]+(?:=[^,\s]+)?(?:,~?[\w\-]+(?:=[^,\s]+)?)*)?$/;
/**
* Regular expression that options on a RegExp filter should match
* @type RegExp
*/
Filter.optionsRegExp = /\$(~?[\w\-]+(?:=[^,\s]+)?(?:,~?[\w\-]+(?:=[^,\s]+)?)*)$/;
/**
* Creates a filter of correct type from its text representation - does the basic parsing and
* calls the right constructor then.
*
* @param {String} text as in Filter()
* @return {Filter} filter or null if the filter couldn't be created
*/
Filter.fromText = function(text)
{
if (!/\S/.test(text))
return null;
if (text in Filter.knownFilters)
return Filter.knownFilters[text];
let ret;
if (Filter.elemhideRegExp.test(text))
ret = ElemHideFilter.fromText(text, RegExp.$1, RegExp.$2, RegExp.$3, RegExp.$4);
else if (text[0] == "!")
ret = new CommentFilter(text);
else
ret = RegExpFilter.fromText(text);
Filter.knownFilters[ret.text] = ret;
return ret;
}
/**
* Deserializes a filter
*
* @param {Object} obj map of serialized properties and their values
* @return {Filter} filter or null if the filter couldn't be created
*/
Filter.fromObject = function(obj)
{
let ret = Filter.fromText(obj.text);
if (ret instanceof ActiveFilter)
{
if ("disabled" in obj)
ret.disabled = (obj.disabled == "true");
if ("hitCount" in obj)
ret.hitCount = parseInt(obj.hitCount) || 0;
if ("lastHit" in obj)
ret.lastHit = parseInt(obj.lastHit) || 0;
}
return ret;
}
/**
* Removes unnecessary whitespaces from filter text, will only return null if
* the input parameter is null.
*/
Filter.normalize = function(/**String*/ text) /**String*/
{
if (!text)
return text;
// Remove line breaks and such
text = text.replace(/[^\S ]/g, "");
if (/^\s*!/.test(text)) {
// Don't remove spaces inside comments
return text.replace(/^\s+/, "").replace(/\s+$/, "");
}
else if (Filter.elemhideRegExp.test(text)) {
// Special treatment for element hiding filters, right side is allowed to contain spaces
/^(.*?)(#+)(.*)$/.test(text); // .split(..., 2) will cut off the end of the string
var domain = RegExp.$1;
var separator = RegExp.$2;
var selector = RegExp.$3;
return domain.replace(/\s/g, "") + separator + selector.replace(/^\s+/, "").replace(/\s+$/, "");
}
else
return text.replace(/\s/g, "");
}
/**
* Class for invalid filters
* @param {String} text see Filter()
* @param {String} reason Reason why this filter is invalid
* @constructor
* @augments Filter
*/
function InvalidFilter(text, reason)
{
Filter.call(this, text);
this.reason = reason;
}
InvalidFilter.prototype =
{
__proto__: Filter.prototype,
/**
* Reason why this filter is invalid
* @type String
*/
reason: null,
/**
* See Filter.serialize()
*/
serialize: function(buffer) {}
};
/**
* Class for comments
* @param {String} text see Filter()
* @constructor
* @augments Filter
*/
function CommentFilter(text)
{
Filter.call(this, text);
}
CommentFilter.prototype =
{
__proto__: Filter.prototype,
/**
* See Filter.serialize()
*/
serialize: function(buffer) {}
};
/**
* Abstract base class for filters that can get hits
* @param {String} text see Filter()
* @param {String} domains (optional) Domains that the filter is restricted to separated by domainSeparator e.g. "foo.com|bar.com|~baz.com"
* @constructor
* @augments Filter
*/
function ActiveFilter(text, domains)
{
Filter.call(this, text);
if (domains)
{
this.domainSource = domains;
this.__defineGetter__("includeDomains", this._getIncludeDomains);
this.__defineGetter__("excludeDomains", this._getExcludeDomains);
}
}
ActiveFilter.prototype =
{
__proto__: Filter.prototype,
/**
* Defines whether the filter is disabled
* @type Boolean
*/
disabled: false,
/**
* Number of hits on the filter since the last reset
* @type Number
*/
hitCount: 0,
/**
* Last time the filter had a hit (in milliseconds since the beginning of the epoch)
* @type Number
*/
lastHit: 0,
/**
* String that the includeDomains and excludeDomains properties should be generated from
* @type String
*/
domainSource: null,
/**
* Separator character used in domainSource property, must be overridden by subclasses
* @type String
*/
domainSeparator: null,
/**
* Map containing domains that this filter should match on or null if the filter should match on all domains
* @type Object
*/
includeDomains: null,
/**
* Map containing domains that this filter should not match on or null if the filter should match on all domains
* @type Object
*/
excludeDomains: null,
/**
* Called first time includeDomains property is requested, triggers _generateDomains method.
*/
_getIncludeDomains: function()
{
this._generateDomains();
return this.includeDomains;
},
/**
* Called first time excludeDomains property is requested, triggers _generateDomains method.
*/
_getExcludeDomains: function()
{
this._generateDomains();
return this.excludeDomains;
},
/**
* Generates includeDomains and excludeDomains properties when one of them is requested for the first time.
*/
_generateDomains: function()
{
let domains = this.domainSource.split(this.domainSeparator);
delete this.domainSource;
delete this.includeDomains;
delete this.excludeDomains;
if (domains.length == 1 && domains[0][0] != "~")
{
// Fast track for the common one-domain scenario
this.includeDomains = {__proto__: null};
this.includeDomains[domains[0]] = true;
}
else
{
for each (let domain in domains)
{
if (domain == "")
continue;
let hash = "includeDomains";
if (domain[0] == "~")
{
hash = "excludeDomains";
domain = domain.substr(1);
}
if (!this[hash])
this[hash] = {__proto__: null};
this[hash][domain] = true;
}
}
},
/**
* Checks whether this filter is active on a domain.
*/
isActiveOnDomain: function(/**String*/ docDomain) /**Boolean*/
{
// If the document has no host name, match only if the filter isn't restricted to specific domains
if (!docDomain)
return (!this.includeDomains);
if (!this.includeDomains && !this.excludeDomains)
return true;
docDomain = docDomain.replace(/\.+$/, "").toUpperCase();
while (true)
{
if (this.includeDomains && docDomain in this.includeDomains)
return true;
if (this.excludeDomains && docDomain in this.excludeDomains)
return false;
let nextDot = docDomain.indexOf(".");
if (nextDot < 0)
break;
docDomain = docDomain.substr(nextDot + 1);
}
return (this.includeDomains == null);
},
/**
* Checks whether this filter is active only on a domain and its subdomains.
*/
isActiveOnlyOnDomain: function(/**String*/ docDomain) /**Boolean*/
{
if (!docDomain || !this.includeDomains)
return false;
docDomain = docDomain.replace(/\.+$/, "").toUpperCase();
for (let domain in this.includeDomains)
if (domain != docDomain && (domain.length <= docDomain.length || domain.indexOf("." + docDomain) != domain.length - docDomain.length - 1))
return false;
return true;
},
/**
* See Filter.serialize()
*/
serialize: function(buffer)
{
if (this.disabled || this.hitCount || this.lastHit)
{
Filter.prototype.serialize.call(this, buffer);
if (this.disabled)
buffer.push("disabled=true");
if (this.hitCount)
buffer.push("hitCount=" + this.hitCount);
if (this.lastHit)
buffer.push("lastHit=" + this.lastHit);
}
}
};
/**
* Abstract base class for RegExp-based filters
* @param {String} text see Filter()
* @param {String} regexpSource filter part that the regular expression should be build from
* @param {Number} contentType (optional) Content types the filter applies to, combination of values from RegExpFilter.typeMap
* @param {Boolean} matchCase (optional) Defines whether the filter should distinguish between lower and upper case letters
* @param {String} domains (optional) Domains that the filter is restricted to, e.g. "foo.com|bar.com|~baz.com"
* @param {Boolean} thirdParty (optional) Defines whether the filter should apply to third-party or first-party content only
* @constructor
* @augments ActiveFilter
*/
function RegExpFilter(text, regexpSource, contentType, matchCase, domains, thirdParty)
{
ActiveFilter.call(this, text, domains);
if (contentType != null)
this.contentType = contentType;
if (matchCase)
this.matchCase = matchCase;
if (thirdParty != null)
this.thirdParty = thirdParty;
if (regexpSource[0] == "/" && regexpSource[regexpSource.length - 1] == "/")
{
// The filter is a regular expression - convert it immediately to catch syntax errors
this.regexp = new RegExp(regexpSource.substr(1, regexpSource.length - 2), this.matchCase ? "" : "i");
}
else
{
// No need to convert this filter to regular expression yet, do it on demand
this.regexpSource = regexpSource;
this.__defineGetter__("regexp", this._generateRegExp);
}
}
RegExpFilter.prototype =
{
__proto__: ActiveFilter.prototype,
/**
* @see ActiveFilter.domainSeparator
*/
domainSeparator: "|",
/**
* Number of filters contained in this filter group
* @type Integer
*/
filterCount: 1,
/**
* Expression from which a regular expression should be generated - for delayed creation of the regexp property
* @type String
*/
regexpSource: null,
/**
* Regular expression to be used when testing against this filter
* @type RegExp
*/
regexp: null,
/**
* 8 character string identifying this filter for faster matching
* @type String
*/
shortcut: null,
/**
* Content types the filter applies to, combination of values from RegExpFilter.typeMap
* @type Number
*/
contentType: 0x7FFFFFFF,
/**
* Defines whether the filter should distinguish between lower and upper case letters
* @type Boolean
*/
matchCase: false,
/**
* Defines whether the filter should apply to third-party or first-party content only. Can be null (apply to all content).
* @type Boolean
*/
thirdParty: null,
/**
* Generates regexp property when it is requested for the first time.
* @return {RegExp}
*/
_generateRegExp: function()
{
// Remove multiple wildcards
let source = this.regexpSource.replace(/\*+/g, "*");
// Remove leading wildcards
if (source[0] == "*")
source = source.substr(1);
// Remove trailing wildcards
let pos = source.length - 1;
if (pos >= 0 && source[pos] == "*")
source = source.substr(0, pos);
source = source.replace(/\^\|$/, "^") // remove anchors following separator placeholder
.replace(/\W/g, "\\$&") // escape special symbols
.replace(/\\\*/g, ".*") // replace wildcards by .*
// process separator placeholders (all ANSI charaters but alphanumeric characters and _%.-)
.replace(/\\\^/g, "(?:[\\x00-\\x24\\x26-\\x2C\\x2F\\x3A-\\x40\\x5B-\\x5E\\x60\\x7B-\\x80]|$)")
.replace(/^\\\|\\\|/, "^[\\w\\-]+:\\/+(?!\\/)(?:[^\\/]+\\.)?") // process extended anchor at expression start
.replace(/^\\\|/, "^") // process anchor at expression start
.replace(/\\\|$/, "$"); // process anchor at expression end
let regexp = new RegExp(source, this.matchCase ? "" : "i");
delete this.regexp;
delete this.regexpSource;
return (this.regexp = regexp);
},
/**
* Tests whether the URL matches this filter
* @param {String} location URL to be tested
* @param {String} contentType content type identifier of the URL
* @param {String} docDomain domain name of the document that loads the URL
* @param {Boolean} thirdParty should be true if the URL is a third-party request
* @return {RegExpFilter} this filter in case of a match or null
*/
matches: function(location, contentType, docDomain, thirdParty)
{
if (this.regexp.test(location) &&
(RegExpFilter.typeMap[contentType] & this.contentType) != 0 &&
(this.thirdParty == null || this.thirdParty == thirdParty) &&
this.isActiveOnDomain(docDomain))
{
return this;
}
return null;
},
/**
* Adds a filter to this filter group
* @param {RegExpFilter} filter to be added
* @return {RegExpFilterGroup} the combined filter group
*/
pushFilter: function(filter)
{
return new RegExpFilterGroup(this, filter);
}
};
/**
* Creates a RegExp filter from its text representation
* @param {String} text same as in Filter()
*/
RegExpFilter.fromText = function(text)
{
let constructor = BlockingFilter;
let origText = text;
if (text.indexOf("@@") == 0)
{
constructor = WhitelistFilter;
text = text.substr(2);
}
let contentType = null;
let matchCase = null;
let domains = null;
let thirdParty = null;
let collapse = null;
let options;
if (Filter.optionsRegExp.test(text))
{
options = RegExp.$1.toUpperCase().split(",");
text = RegExp.leftContext;
for each (let option in options)
{
let value;
[option, value] = option.split("=", 2);
option = option.replace(/-/, "_");
if (option in RegExpFilter.typeMap)
{
if (contentType == null)
contentType = 0;
contentType |= RegExpFilter.typeMap[option];
}
else if (option[0] == "~" && option.substr(1) in RegExpFilter.typeMap)
{
if (contentType == null)
contentType = RegExpFilter.prototype.contentType;
contentType &= ~RegExpFilter.typeMap[option.substr(1)];
}
else if (option == "MATCH_CASE")
matchCase = true;
else if (option == "DOMAIN" && typeof value != "undefined")
domains = value;
else if (option == "THIRD_PARTY")
thirdParty = true;
else if (option == "~THIRD_PARTY")
thirdParty = false;
else if (option == "COLLAPSE")
collapse = true;
else if (option == "~COLLAPSE")
collapse = false;
}
}
if (constructor == WhitelistFilter && (contentType == null || (contentType & RegExpFilter.typeMap.DOCUMENT)) &&
(!options || options.indexOf("DOCUMENT") < 0) && !/^\|?[\w\-]+:/.test(text))
{
// Exception filters shouldn't apply to pages by default unless they start with a protocol name
if (contentType == null)
contentType = RegExpFilter.prototype.contentType;
contentType &= ~RegExpFilter.typeMap.DOCUMENT;
}
try
{
return new constructor(origText, text, contentType, matchCase, domains, thirdParty, collapse);
}
catch (e)
{
return new InvalidFilter(text, e);
}
}
/**
* Maps type strings like "SCRIPT" or "OBJECT" to bit masks
*/
RegExpFilter.typeMap = {
OTHER: 1,
SCRIPT: 2,
IMAGE: 4,
STYLESHEET: 8,
OBJECT: 16,
SUBDOCUMENT: 32,
DOCUMENT: 64,
XBL: 512,
PING: 1024,
XMLHTTPREQUEST: 2048,
OBJECT_SUBREQUEST: 4096,
DTD: 8192,
MEDIA: 16384,
FONT: 32768,
BACKGROUND: 4, // Backwards compat, same as IMAGE
DONOTTRACK: 0x20000000,
ELEMHIDE: 0x40000000
};
// ELEMHIDE and DONOTTRACK option shouldn't be there by default
RegExpFilter.prototype.contentType &= ~(RegExpFilter.typeMap.ELEMHIDE | RegExpFilter.typeMap.DONOTTRACK);
/**
* Class for blocking filters
* @param {String} text see Filter()
* @param {String} regexpSource see RegExpFilter()
* @param {Number} contentType see RegExpFilter()
* @param {Boolean} matchCase see RegExpFilter()
* @param {String} domains see RegExpFilter()
* @param {Boolean} thirdParty see RegExpFilter()
* @param {Boolean} collapse defines whether the filter should collapse blocked content, can be null
* @constructor
* @augments RegExpFilter
*/
function BlockingFilter(text, regexpSource, contentType, matchCase, domains, thirdParty, collapse)
{
RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, thirdParty);
this.collapse = collapse;
}
BlockingFilter.prototype =
{
__proto__: RegExpFilter.prototype,
/**
* Defines whether the filter should collapse blocked content. Can be null (use the global preference).
* @type Boolean
*/
collapse: null
};
/**
* Class for whitelist filters
* @param {String} text see Filter()
* @param {String} regexpSource see RegExpFilter()
* @param {Number} contentType see RegExpFilter()
* @param {Boolean} matchCase see RegExpFilter()
* @param {String} domains see RegExpFilter()
* @param {Boolean} thirdParty see RegExpFilter()
* @constructor
* @augments RegExpFilter
*/
function WhitelistFilter(text, regexpSource, contentType, matchCase, domains, thirdParty)
{
RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, thirdParty);
}
WhitelistFilter.prototype =
{
__proto__: RegExpFilter.prototype
}
/**
* Class for element hiding filters
* @param {String} text see Filter()
* @param {String} domains (optional) Host names or domains the filter should be restricted to
* @param {String} selector CSS selector for the HTML elements that should be hidden
* @constructor
* @augments ActiveFilter
*/
function ElemHideFilter(text, domains, selector)
{
ActiveFilter.call(this, text, domains ? domains.toUpperCase() : null);
if (domains)
this.selectorDomain = domains.replace(/,~[^,]+/g, "").replace(/^~[^,]+,?/, "").toLowerCase();
this.selector = selector;
}
ElemHideFilter.prototype =
{
__proto__: ActiveFilter.prototype,
/**
* @see ActiveFilter.domainSeparator
*/
domainSeparator: ",",
/**
* Host name or domain the filter should be restricted to (can be null for no restriction)
* @type String
*/
selectorDomain: null,
/**
* CSS selector for the HTML elements that should be hidden
* @type String
*/
selector: null,
/**
* Random key associated with the filter - used to register hits from element hiding filters
* @type String
*/
key: null
};
/**
* Creates an element hiding filter from a pre-parsed text representation
*
* @param {String} text same as in Filter()
* @param {String} domain domain part of the text representation (can be empty)
* @param {String} tagName tag name part (can be empty)
* @param {String} attrRules attribute matching rules (can be empty)
* @param {String} selector raw CSS selector (can be empty)
* @return {ElemHideFilter or InvalidFilter}
*/
ElemHideFilter.fromText = function(text, domain, tagName, attrRules, selector)
{
if (!selector)
{
if (tagName == "*")
tagName = "";
let id = null;
let additional = "";
if (attrRules) {
attrRules = attrRules.match(/\([\w\-]+(?:[$^*]?=[^\(\)"]*)?\)/g);
for each (let rule in attrRules) {
rule = rule.substr(1, rule.length - 2);
let separatorPos = rule.indexOf("=");
if (separatorPos > 0) {
rule = rule.replace(/=/, '="') + '"';
additional += "[" + rule + "]";
}
else {
if (id)
return new InvalidFilter(text, Utils.getString("filter_elemhide_duplicate_id"));
else
id = rule;
}
}
}
if (id)
selector = tagName + "." + id + additional + "," + tagName + "#" + id + additional;
else if (tagName || additional)
selector = tagName + additional;
else
return new InvalidFilter(text, Utils.getString("filter_elemhide_nocriteria"));
}
return new ElemHideFilter(text, domain, selector);
}
function RegExpFilterGroup(filter1, filter2)
{
this.filters = [filter1, filter2];
}
RegExpFilterGroup.prototype = {
/**
* Filters contained in this filter group.
* @type Array of RegExpFilter
*/
filters: null,
/**
* Number of filters contained in this filter group
* @type Integer
*/
filterCount: 2,
/**
* Tests whether the URL matches any of the filters
* @param {String} location URL to be tested
* @param {String} contentType content type identifier of the URL
* @param {String} docDomain domain name of the document that loads the URL
* @param {Boolean} thirdParty should be true if the URL is a third-party request
* @return {RegExpFilter} the filter matching the URL or null if none of the filters match
*/
matches: function(location, contentType, docDomain, thirdParty)
{
for (let i = 0, l = this.filters.length; i < l; i++)
{
let result = this.filters[i].matches(location, contentType, docDomain, thirdParty);
if (result)
return result;
}
return null;
},
/**
* Adds a filter to this filter group
* @param {RegExpFilter} filter to be added
* @return {RegExpFilterGroup} the combined filter group
*/
pushFilter: function(filter)
{
this.filters.push(filter);
this.count++;
return this;
}
}
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Adblock Plus.
*
* The Initial Developer of the Original Code is
* Wladimir Palant.
* Portions created by the Initial Developer are Copyright (C) 2006-2010
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
*
* ***** END LICENSE BLOCK ***** */
/**
* @fileOverview Matcher class implementing matching addresses against a list of filters.
*/
var EXPORTED_SYMBOLS = ["Matcher", "CombinedMatcher", "defaultMatcher"];
/**
* Blacklist/whitelist filter matching
* @constructor
*/
function Matcher()
{
this.clear();
}
Matcher.prototype = {
/**
* Lookup table for filters by their shortcut
* @type Object
*/
shortcutHash: null,
/**
* Should be true if shortcutHash has any entries
* @type Boolean
*/
hasShortcuts: false,
/**
* Filters without a shortcut
* @type Array of RegExpFilter
*/
regexps: null,
/**
* Lookup table, has keys for all filters already added
* @type Object
*/
knownFilters: null,
/**
* Removes all known filters
*/
clear: function()
{
this.shortcutHash = {__proto__: null};
this.hasShortcuts = false;
this.regexps = [];
this.knownFilters = {__proto__: null};
},
/**
* Adds a filter to the matcher
* @param {RegExpFilter} filter
*/
add: function(filter)
{
if (filter.text in this.knownFilters)
return;
// Look for a suitable shortcut if the filter doesn't have one
if (!filter.shortcut)
filter.shortcut = this.findShortcut(filter);
if (filter.shortcut)
{
if (filter.shortcut in this.shortcutHash)
this.shortcutHash[filter.shortcut] = this.shortcutHash[filter.shortcut].pushFilter(filter);
else
this.shortcutHash[filter.shortcut] = filter;
this.hasShortcuts = true;
}
else
this.regexps.push(filter);
this.knownFilters[filter.text] = true;
},
/**
* Removes a filter from the matcher
* @param {RegExpFilter} filter
*/
remove: function(filter)
{
if (!(filter.text in this.knownFilters))
return;
if (filter.shortcut)
{
if ("filters" in this.shortcutHash[filter.shortcut])
{
let list = this.shortcutHash[filter.shortcut].filters;
for (let i = 0, l = list.length; i < l; i++)
if (list[i] == filter)
list.splice(i--, 1);
}
else
delete this.shortcutHash[filter.shortcut];
}
else
{
let i = this.regexps.indexOf(filter);
if (i >= 0)
this.regexps.splice(i, 1);
}
delete this.knownFilters[filter.text];
},
/**
* Looks up a free shortcut for a filter
* @param {String} text text representation of the filter
* @return {String} shortcut or null
*/
findShortcut: function(filter)
{
// For donottrack filters use "donottrack" as keyword if nothing else matches
let defaultResult = (filter.contentType & RegExpFilter.typeMap.DONOTTRACK ? "donottrack" : null);
let text = filter.text;
if (Filter.regexpRegExp.test(text))
return defaultResult;
// Remove options
if (Filter.optionsRegExp.test(text))
text = RegExp.leftContext;
// Remove whitelist marker
if (text.substr(0, 2) == "@@")
text = text.substr(2);
let candidates = text.toLowerCase().match(/[^a-z0-9%*][a-z0-9%]{3,}(?=[^a-z0-9%*])/g);
if (!candidates)
return defaultResult;
let hash = this.shortcutHash;
let result = defaultResult;
let resultCount = 0xFFFFFF;
let resultLength = 0;
for (let i = 0, l = candidates.length; i < l; i++)
{
let candidate = candidates[i].substr(1);
let count = candidate in hash ? hash[candidate].filterCount : 0;
if (count < resultCount || (count == resultCount && candidate.length > resultLength))
{
result = candidate;
resultCount = count;
resultLength = candidate.length;
}
}
return result;
},
/**
* Tests whether the URL matches any of the known filters
* @param {String} location URL to be tested
* @param {String} contentType content type identifier of the URL
* @param {String} docDomain domain name of the document that loads the URL
* @param {Boolean} thirdParty should be true if the URL is a third-party request
* @return {RegExpFilter} matching filter or null
*/
matchesAny: function(location, contentType, docDomain, thirdParty)
{
if (this.hasShortcuts)
{
// Optimized matching using shortcuts
let candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g);
if (contentType == "DONOTTRACK")
candidates.unshift("donottrack");
if (candidates)
{
for (let i = 0, l = candidates.length; i < l; i++)
{
let substr = candidates[i];
if (substr in this.shortcutHash)
{
let result = this.shortcutHash[substr].matches(location, contentType, docDomain, thirdParty);
if (result)
return result;
}
}
}
}
// Slow matching for filters without shortcut
for each (let filter in this.regexps)
if (filter.matches(location, contentType, docDomain, thirdParty))
return filter;
return null;
}
};
/**
* Combines a matcher for blocking and exception rules, automatically sorts
* rules into two Matcher instances.
* @constructor
*/
function CombinedMatcher()
{
this.blacklist = new Matcher();
this.whitelist = new Matcher();
this.resultCache = {__proto__: null};
}
/**
* Maximal number of matching cache entries to be kept
* @type Number
*/
CombinedMatcher.maxCacheEntries = 1000;
CombinedMatcher.prototype =
{
/**
* Matcher for blocking rules.
* @type Matcher
*/
blacklist: null,
/**
* Matcher for exception rules.
* @type Matcher
*/
whitelist: null,
/**
* Lookup table of previous matchesAny results
* @type Object
*/
resultCache: null,
/**
* Number of entries in resultCache
* @type Number
*/
cacheEntries: 0,
/**
* @see Matcher#clear
*/
clear: function()
{
this.blacklist.clear();
this.whitelist.clear();
this.resultCache = {__proto__: null};
this.cacheEntries = 0;
},
/**
* @see Matcher#add
*/
add: function(filter)
{
if (filter instanceof WhitelistFilter)
this.whitelist.add(filter);
else
this.blacklist.add(filter);
if (this.cacheEntries > 0)
{
this.resultCache = {__proto__: null};
this.cacheEntries = 0;
}
},
/**
* @see Matcher#remove
*/
remove: function(filter)
{
if (filter instanceof WhitelistFilter)
this.whitelist.remove(filter);
else
this.blacklist.remove(filter);
if (this.cacheEntries > 0)
{
this.resultCache = {__proto__: null};
this.cacheEntries = 0;
}
},
/**
* @see Matcher#findShortcut
*/
findShortcut: function(filter)
{
if (filter instanceof WhitelistFilter)
return this.whitelist.findShortcut(filter);
else
return this.blacklist.findShortcut(filter);
},
/**
* Optimized filter matching testing both whitelist and blacklist matchers
* simultaneously. For parameters see Matcher.matchesAny().
* @see Matcher#matchesAny
*/
matchesAnyInternal: function(location, contentType, docDomain, thirdParty)
{
let blacklistHit = null;
if (this.whitelist.hasShortcuts || this.blacklist.hasShortcuts)
{
// Optimized matching using shortcuts
let hashWhite = this.whitelist.shortcutHash;
let hashBlack = this.blacklist.shortcutHash;
let candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g);
if (contentType == "DONOTTRACK")
candidates.unshift("donottrack");
if (candidates)
{
for (let i = 0, l = candidates.length; i < l; i++)
{
let substr = candidates[i];
if (substr in hashWhite)
{
let result = hashWhite[substr].matches(location, contentType, docDomain, thirdParty);
if (result)
return result;
}
if (substr in hashBlack)
{
let result = hashBlack[substr].matches(location, contentType, docDomain, thirdParty);
if (result)
blacklistHit = result;
}
}
}
}
// Slow matching for filters without shortcut
for each (let filter in this.whitelist.regexps)
if (filter.matches(location, contentType, docDomain, thirdParty))
return filter;
if (blacklistHit)
return blacklistHit;
for each (let filter in this.blacklist.regexps)
if (filter.matches(location, contentType, docDomain, thirdParty))
return filter;
return null;
},
/**
* @see Matcher#matchesAny
*/
matchesAny: function(location, contentType, docDomain, thirdParty)
{
let key = location + " " + contentType + " " + docDomain + " " + thirdParty;
if (key in this.resultCache)
return this.resultCache[key];
let result = this.matchesAnyInternal(location, contentType, docDomain, thirdParty);
if (this.cacheEntries >= CombinedMatcher.maxCacheEntries)
{
this.resultCache = {__proto__: null};
this.cacheEntries = 0;
}
this.resultCache[key] = result;
this.cacheEntries++;
return result;
}
}
/**
* Shared CombinedMatcher instance that should usually be used.
* @type CombinedMatcher
*/
var defaultMatcher = new CombinedMatcher();
/**
google search filter plus
*/
/**
extension of adblock plus Filter class
provides subfilter classes, filter storage
*/
let gfpFilter={
//split options regexp
optionsRegExp: /\$([\w,]*?)(?:\$|$)/,
//getter methods
isDummy: function(filter){
return 'text' in filter;
},
isDuplicateText: function(text1,text2){
//checking startsWith is enough, since different options are considered as different filters
if(text1.length>text2.length){
return text1.substr(0,text2.length)==text2;
}else{
return text2.substr(0,text1.length)==text1;
}
},
isDuplicate: function(keys,filters){
/**
checks if filter has a duplicate in filters
e.g.
a$$b is a duplicate of a$$b$$c and vice versa
a$$b$$c is not a duplicate of a$$b$$d
returns:
deepest duplicate level reached (<=0 if no duplicates found, positive otherwise)
*/
let filter;
if(keys[0] && (keys[0] in filters)){
filter=filters[keys[0]];
}else{
return 0;
}
for(let i=1;i<keys.length;i++){
if('subfilters' in filter){
filters=filter.subfilters;
}else{
return i;
}
if(keys[i] in filters){
filter=filters[keys[i]];
}else{
return -i;
}
}
return keys.length;
},
getSubfilter: function(keys,filters,pop){
/**
gets a subfilter from filters
assumes filters are compiled
doesn't assume keys are all valid
*/
let filter;
if(keys[0] && (keys[0] in filters)){
filter=filters[keys[0]];
}else{
return null;
}
for(let i=1;i<keys.length;i++){
if('subfilters' in filter){
filters=filter.subfilters;
}else{
return null;
}
if(keys[i] in filters){
filter=filters[keys[i]];
}else{
return null;
}
}
if(pop){
delete filters[keys[keys.length-1]];
}
return filter;
},
popSubfilter: function(keys,filters){
/**
assumes filters are compiled
*/
return gfpFilter.getSubfilter(keys,filters,true);
},
isSlowFilter: function(filter,filterParts){
/**
args:
filter: last subfilter
filterParts: parts of filter excluding filter
*/
if((filterParts[0]||filter) instanceof CommentFilter) return false;
for(let i=0;i<filterParts.length;i++){
if(filterParts[i] instanceof InvalidFilter) return false;
if(defaultMatcher.findShortcut(filterParts[i])==null) return true;
}
if(filter instanceof InvalidFilter) return false;
if(defaultMatcher.findShortcut(filter)==null) return true;
return false;
},
isSlowFilterKeys: function(keys,filters){
/**
assumes filters are compiled
assumes keys exist in filters
args:
filter: a filter with maximum of 1 subfilter
*/
let filter=filters[keys[0]];
if(filter instanceof CommentFilter) return false;
for(let i=1;;i++){
if(filter instanceof InvalidFilter) return false;
if(defaultMatcher.findShortcut(filter)==null) return true;
if(i==keys.length) return false;
if(!('subfilters' in filter)) return false;
filter=filter.subfilters[keys[i]];
}
},
getKeys: function(text){
/**
get subfilter keys from filterText
*/
if(!/\S/.test(text)){
return [];
}
if(text[0]=='!'){
return [text];
}
let ret=[];
let textParts=gfpFilter.str2textParts(text);
while(textParts.length!=0){
let text=textParts.shift();
let option=textParts.shift();
if(option){
ret.push(text+'$'+option);
}else{
ret.push(text);
}
}
return ret;
},
iterate: function(filters,callback,filterParts){
/**
iterates over filters
assumes filters are compiled
callback: [filter,filterParts]
*/
if(filterParts==undefined) filterParts=[];
for each(let filter in filters){
if('subfilters' in filter){
filterParts.push(filter);
gfpFilter.iterate(filter.subfilters,callback,filterParts);
filterParts.pop();
}else{
callback(filter,filterParts);
}
}
},
//setter methods
str2options: function(option,filter){
let options=option.toUpperCase().split(',');
for(let i=0;i<options.length;i++){
switch(options[i]){
case 'MATCH_CASE':
filter.matchCase=true;
break;
default:
return new InvalidFilter(filter.text,'invalid filter option');
}
}
return filter;
},
str2textParts: function(text){
let ret=text.split(gfpFilter.optionsRegExp);
if(ret[ret.length-1].length==0){
ret.pop();
}
return ret;
},
fromTextParts: function(text,option,filters){
/**
converts a single filter part from text, option
stores filter into filters
*/
if(option){
text+='$'+option;
}
if(text in filters){
return filters[text];
}else{
let filter=RegExpFilter.fromText(text);
if(option){
filter.text=text;
filter=gfpFilter.str2options(option,filter);
}
filters[text]=filter;
return filter;
}
},
fromText: function(text,filters){
/**
assumes no filters are compiled
returns:
if has subfilters, dummy object for filter (end of textParts)
if no subfilters, actual filter object
*/
if(!/\S/.test(text)){
return null;
}
if(text[0]=='!'){
let filter=new CommentFilter(text);
filters[text]=filter;
return filter;
}
let textParts=gfpFilter.str2textParts(text);
let filter=gfpFilter.fromTextParts(textParts.shift(),textParts.shift(),filters);
if(filter instanceof InvalidFilter){
return filter;
}
if(textParts.length==0){
return filter;
}
if(!('subfilters' in filter)){
filter.subfilters={};
filter.compiled=false;
}
let dummyFilter={};
textParts.push(dummyFilter);
filter.subfilters[text]=textParts;
return dummyFilter;
},
fromTextCompiled: function(text,filters){
/**
filters can be in any compiled state
prefers subfilters to be compiled
returns:
last subfilter
*/
if(!/\S/.test(text)){
return null;
}
if(text[0]=='!'){
let filter=new CommentFilter(text);
filters[text]=filter;
return filter;
}
let textParts=gfpFilter.str2textParts(text);
for(let i=0;;i=1){
let filter=gfpFilter.fromTextParts(textParts.shift(),textParts.shift(),filters);
if(filter instanceof InvalidFilter){
return filter;
}
if(textParts.length==0){
if(i==1){
filter.fullText=text;
}
//remove subfilters so the added filter isn't a parent filter
delete filter.subfilters;
return filter;
}
if('subfilters' in filter){
if(!filter.compiled){
let dummyFilter={};
textParts.push(dummyFilter);
filter.subfilters[text]=textParts;
return dummyFilter;
}
}else{
filter.subfilters={};
filter.compiled=true;
}
filters=filter.subfilters;
}
},
clone: function(filter){
/**
clones a subfilter without cloning its subfilters
*/
let ret=RegExpFilter.fromText(filter.text);
if(ret instanceof ActiveFilter){
if('disabled' in filter)
ret.disabled=filter.disabled;
if('hitCount' in filter)
ret.hitCount=filter.hitCount;
if('lastHit' in filter)
ret.lastHit=filter.lastHit;
}
return ret;
},
setSubfilter: function(n,keys,filters){
/**
sets a subfilter to a value with given keys
assumes that keys exist
returns:
final subfilter
*/
for(let i=0;i<keys.length-1;i++){
filters=filters[keys[i]].subfilters;
}
filters[keys[keys.length-1]]=n;
return n;
},
compileSubfilters: function(filter){
let filters=filter.subfilters;
let ret={};
for(let text in filters){
let textParts=filters[text];
let l=textParts.length;
let filter;
if(l==2){
filter=gfpFilter.fromTextParts(textParts.shift(),'',ret);
}else{
filter=gfpFilter.fromTextParts(textParts.shift(),textParts.shift(),ret);
}
if(filter instanceof InvalidFilter){
continue;
}
if(l<=3){
//copy properties of dummyFilter into actual filter
let dummyFilter=textParts.shift();
for(let key in dummyFilter){
filter[key]=dummyFilter[key];
}
filter.fullText=text;
continue;
}
if(!('subfilters' in filter)){
filter.subfilters={};
filter.compiled=false;
}
filter.subfilters[text]=textParts;
}
filter.compiled=true;
filter.subfilters=ret;
return ret;
},
compileAll: function(filters){
/**
compiles all filters
*/
for each(let filter in filters){
if('subfilters' in filter){
if(!filter.compiled){
gfpFilter.compileAll(gfpFilter.compileSubfilters(filter));
}
}
}
return filters;
},
getSubfilters: function(filter){
if(filter.compiled) return filter.subfilters;
return gfpFilter.compileSubfilters(filter);
},
//string operations
stringifyRawS: function(filters,buffer,simpleStore){
/**
stringify a sub, uncompiled filter
*/
let serialize=ActiveFilter.prototype.serialize;
for(let text in filters){
let filter=filters[text];
filter=filter[filter.length-1];
let o_len=buffer.length;
filter.text=escape(text);
serialize.call(filter,buffer);
if(buffer.length==o_len){
simpleStore.push(text);
}
}
},
stringifyRawC: function(filters,buffer,simpleStore,isSub){
/**
stringify a compiled filter
*/
for each(let filter in filters){
if('subfilters' in filter){
if(filter.compiled){
gfpFilter.stringifyRawC(filter.subfilters,buffer,simpleStore,true);
}else{
gfpFilter.stringifyRawS(filter.subfilters,buffer,simpleStore);
}
}else{
let o_len=buffer.length;
let text=isSub?filter.fullText:filter.text;
filter.text=escape(text);
filter.serialize(buffer);
filter.text=text;
if(buffer.length==o_len){
//simpleStore does not need escaping
simpleStore.push(text);
}
}
}
},
stringify: function(filters){
let buffer=[];
let simpleStore=['[Simple Store]'];
gfpFilter.stringifyRawC(filters,buffer,simpleStore,false);
let ret='';
if(buffer.length!=0) ret+=buffer.join('\n')+'\n';
ret+=simpleStore.join('\n');
return ret;
},
_parse: function(s,fromObject){
/**
verifies s
returns: parsed filters
*/
let ret={};
let buffer=s.split('\n');
let i=0;
for(i=0;i<buffer.length;){
if(buffer[i]=='[Filter]'){
let filter={};
for(i++;buffer[i][0]!='[' && i<buffer.length;i++){
let [key,val]=buffer[i].split('=');
filter[key]=val;
}
if(filter.text==undefined){
throw 'invalid serialized string';
}
filter.text=unescape(filter.text);
fromObject(filter,ret);
}else if(buffer[i]=='[Simple Store]'){
i++;
break;
}else{
throw 'invalid serialized string';
}
}
if(i==0 && i<buffer.length){
throw 'invalid serialized string';
}
for(;i<buffer.length;i++){
gfpFilter.fromText(buffer[i],ret);
}
return ret;
},
parse: function(s){
return gfpFilter._parse(s,gfpFilter.fromObject);
},
parseCompiled: function(s){
return gfpFilter._parse(s,gfpFilter.fromObjectCompiled);
},
//Filter class hooks
fromObject: function(obj,filters){
let ret=gfpFilter.fromText(obj.text,filters);
if(ret instanceof ActiveFilter || ret.constructor==Object){
if('disabled' in obj)
ret.disabled=(obj.disabled=='true');
if('hitCount' in obj)
ret.hitCount=parseInt(obj.hitCount)||0;
if('lastHit' in obj)
ret.lastHit=parseInt(obj.lastHit)||0;
}
return ret;
},
fromObjectCompiled: function(obj,filters){
let ret=gfpFilter.fromTextCompiled(obj.text,filters);
if(!ret) return null;
if(ret instanceof ActiveFilter || ret.constructor==Object){
if('disabled' in obj)
ret.disabled=(obj.disabled=='true');
if('hitCount' in obj)
ret.hitCount=parseInt(obj.hitCount)||0;
if('lastHit' in obj)
ret.lastHit=parseInt(obj.lastHit)||0;
}
return ret;
},
//storage
flush: function(filters){
/**
flushes filters to Filter.knownFilters
*/
Filter.knownFilters=filters;
},
isPtr: function(filters){
return Filter.knownFilters==filters;
},
save: function(){
GM_setValue('filters',gfpFilter.stringify(Filter.knownFilters));
},
load: function(){
let s=GM_getValue('filters');
if(s==undefined){
//load filters from default config
let textFilters=config.filters;
let filters=Filter.knownFilters;
for(let i=0;i<textFilters.length;i++){
gfpFilter.fromText(textFilters[i],filters);
}
gfpFilter.save();
}else{
gfpFilter.flush(gfpFilter.parse(s));
}
},
//init
init: function(){
//save some time by removing some abp parsing
let emptyRegex={'test': function() false};
Filter.optionsRegExp=emptyRegex;
Filter.elemhideRegExp=emptyRegex;
gfpFilter.load();
},
}
/**
core filter matcher operations
uses adblock plus Matcher class
*/
let gfpMatcher={
add: function(filter,keys){
/**
adds filter matcher to parent filter's submatcher, if it exists
checks if filter is a dummy filter
args:
keys: filter's keys
*/
if(!(filter instanceof ActiveFilter)) return;
if(keys.length==1){
defaultMatcher.add(filter);
return;
}
defaultMatcher.add(Filter.knownFilters[keys[0]]);
if(!gfpFilter.isDummy(filter)){
//filter is not a dummy filter, parent is compiled
keys.pop();
let parentFilter=gfpFilter.getSubfilter(keys);
if('submatcher' in parentFilter){
parentFilter.submatcher.add(filter);
}
}
},
getSubmatcher: function(filter){
if('submatcher' in filter) return filter.submatcher;
let submatcher=new CombinedMatcher();
for each(let subfilter in gfpFilter.getSubfilters(filter)){
if(filter instanceof ActiveFilter){
submatcher.add(subfilter);
}
}
filter.submatcher=submatcher;
return submatcher;
},
matchesAny: function(nodeData){
return this._matchesAny(nodeData.url,nodeData,gfpMatcher.matchesUrl);
},
matchesUrl: function(filter,url,nodeData){
/**
replaces ActiveFilter.matches
*/
if(!filter.regexp.test(url)) return null;
if(!('subfilters' in filter)) return filter;
//check if matches title
return gfpMatcher.getSubmatcher(filter)._matchesAny(nodeData.title,nodeData,gfpMatcher.matchesTitle);
},
matchesTitle: function(filter,title,nodeData){
/**
replaces ActiveFilter.matches
*/
if(!filter.regexp.test(title)) return null;
if(!('subfilters' in filter)) return filter;
//check if matches summary
return gfpMatcher.getSubmatcher(filter)._matchesAny(nodeData.summary,nodeData,gfpMatcher.matchesSummary);
},
matchesSummary: function(filter,summary,nodeData){
/**
replaces ActiveFilter.matches
*/
if(!filter.regexp.test(summary)) return null;
return filter;
},
_matchesAny: function(text,nodeData,matchesFunc){
if(!text) return null;
let blacklistHit=null;
if(this.whitelist.hasShortcuts || this.blacklist.hasShortcuts){
//optimized matching using shortcuts
let hashWhite=this.whitelist.shortcutHash;
let hashBlack=this.blacklist.shortcutHash;
let candidates=text.toLowerCase().match(/[a-z0-9%]{3,}/g);
if(candidates){
for(let i=0,l=candidates.length;i<l;i++){
let substr=candidates[i];
if(substr in hashWhite){
let ret=hashWhite[substr].matches(text,nodeData,matchesFunc);
if(ret) return ret;
}
if(substr in hashBlack && !blacklistHit){
let ret=hashBlack[substr].matches(text,nodeData,matchesFunc);
if(ret) blacklistHit=ret;
}
}
}
}
//slow matching for filters without shortcut
for each(let filter in this.whitelist.regexps){
let ret=matchesFunc(filter,text,nodeData);
if(ret) return ret;
}
if(blacklistHit) return blacklistHit;
for each(let filter in this.blacklist.regexps){
let ret=matchesFunc(filter,text,nodeData);
if(ret) return ret;
}
return null;
},
init: function(){
//initialize defaultMatcher
for each(let filter in Filter.knownFilters){
if(filter instanceof ActiveFilter && !filter.disabled){
defaultMatcher.add(filter);
}
}
},
}
CombinedMatcher.prototype.matchesAny=gfpMatcher.matchesAny;
CombinedMatcher.prototype._matchesAny=gfpMatcher._matchesAny;
RegExpFilterGroup.prototype.matches=function(text,nodeData,matchesFunc){
for(let i=0,l=this.filters.length;i<l;i++){
let ret=matchesFunc(this.filters[i],text,nodeData);
if(ret) return ret;
}
return null;
}
RegExpFilter.prototype.matches=function(text,nodeData,matchesFunc){
return matchesFunc(this,text,nodeData);
}
/**
pref metadata class
*/
let prefMeta={
isUpdated: false,
};
/**
search results gui
hiding search results, adding filter dialog
*/
let searchGui={
resHidden: false,
remNodes: null,
initialized: false,
isHomePage: function(){
let loc=window.location.href;
return loc[loc.length-1]=='/';
},
getQuery: function(){
return document.querySelector('input[type="text"][title="Search"]');
},
getResults: function(){
return _$('GoogleTabledResults') || _$('res');
},
directLink: function(url){
if(!(url[0]=='/')) return url;
return url.substring(7,url.indexOf('&')||url.length);
},
r: {
res: {
getResults: function(node) node.querySelectorAll('li.g'),
getLinkArea: function() null,
getUrl: function() null,
getTitle: function() null,
getSummary: function() null,
},
text: {
getResults: function() null,
getLinkArea: function(node) node.querySelector('.vshid'),
getUrl: function(node) node.querySelector('a.l').href,
getTitle: function(node) (node.querySelector('h2.r')||node.querySelector('h3.r')).textContent,
getSummary: function(node) node.querySelector('div.s').textContent,
},
book: {
getResults: function() null,
getLinkArea: function(node) node.querySelector('cite').parentNode,
getUrl: function(node) node.querySelector('h3.r>a').href,
getTitle: function(node) node.querySelector('h3.r').textContent,
getSummary: function(node) node.querySelector('div.s').textContent,
},
videoCtn: {
getResults: function(node) node.querySelectorAll('div.vresult'),
getLinkArea: function() null,
getUrl: function() null,
getTitle: function(node) node.querySelector('h3.r').textContent,
getSummary: function() null,
},
video: {
getResults: function() null,
getLinkArea: function(node) node.querySelector('cite').parentNode,
getUrl: function(node) node.querySelector('a.l').href,
getTitle: function(node) node.querySelector('a.l').textContent,
getSummary: function(node) node.querySelector('span.f').textContent,
},
videoBig: {
getResults: function() null,
getLinkArea: function(node) node.querySelector('span.gl'),
getUrl: function(node) node.querySelector('a.l').href,
getTitle: function(node) node.querySelector('a.l').textContent,
getSummary: function(node) node.querySelector('span.f').parentNode.textContent,
},
imageCtn: {
getResults: function(node) node.querySelectorAll('div>a'),
getLinkArea: function() null,
getUrl: function() null,
getTitle: function(node) node.querySelector('h3.r').textContent,
getSummary: function() null,
},
image: {
getResults: function() null,
getLinkArea: function() null,
getUrl: function(node) node.href,
getTitle: function() null,
getSummary: function() null,
},
newsCtn: {
getResults: function(node) node.querySelectorAll('li.w0>div'),
getLinkArea: function() null,
getUrl: function() null,
getTitle: function(node) node.querySelector('h3.r').textContent,
getSummary: function() null,
},
news: {
getResults: function() null,
getLinkArea: function(node) node.querySelector('.gl'),
getUrl: function(node) node.querySelector('a.l').href,
getTitle: function(node) node.querySelector('a.l').textContent,
getSummary: function(node) node.querySelector('div[style]').textContent,
},
},
nodeData: function(node,filterClass){
this.__defineGetter__('linkArea',function(){let linkArea=filterClass.getLinkArea(node); this.linkArea=linkArea; return linkArea;});
this.__defineGetter__('url',function(){let url=filterClass.getUrl(node); this.url=url; return url;});
this.__defineGetter__('title',function(){let title=filterClass.getTitle(node); this.title=title; return title;});
this.__defineGetter__('summary',function(){let summary=filterClass.getSummary(node); this.summary=summary; return summary;});
},
remNode: function(node,filterClass){
this.node=node;
this.filterClass=filterClass;
},
getResultType: function(node,filterClass){
/**
args:
filterClass: parent result filter class
returns: resultClass
*/
switch(filterClass){
case searchGui.r.videoCtn:
return searchGui.r.video;
case searchGui.r.imageCtn:
return searchGui.r.image;
case searchGui.r.newsCtn:
return searchGui.r.news;
}
switch(node.id){
case 'res':
case 'GoogleTabledResults':
return searchGui.r.res;
case 'imagebox':
return searchGui.r.imageCtn;
case 'videobox':
return searchGui.r.videoCtn;
case 'newsbox':
return searchGui.r.newsCtn;
}
switch(node.getAttribute('class')){
case 'g':
if(node.firstElementChild.nodeName=='H3'){
return searchGui.r.book;
}
return searchGui.r.text;
case 'g videobox':
case 'g videobox w0':
return searchGui.r.videoBig;
}
return null;
},
addStyles: function(){
GM_addStyle(
'.filterAdd{color: #1122CC !important; font-size: 90%; text-decoration: none;} .filterAdd:hover{text-decoration: underline;}'+
'.showTitle{color: #999999 !important; font-size="90%";}'+
'.showLink{color: #999999 !important; font-size="90%"; text-decoration: none;}}'
);
},
initNodes: function(){
/**
buffer nodes
*/
//dash
let dash=document.createElement('span');
with(dash){
innerHTML=' - ';
}
searchGui.dash=dash;
//add filter link
let addLink=document.createElement('a');
with(addLink){
innerHTML='Filter';
href='javascript:void(0);';
setAttribute('class','filterAdd');
}
searchGui.addLink=addLink;
//add filter container
let addCtn=document.createElement('span');
with(addCtn){
appendChild(dash.cloneNode(true));
appendChild(addLink.cloneNode(true));
}
searchGui.addCtn=addCtn;
//hidden result title
let showTitle=document.createElement('span');
with(showTitle){
setAttribute('class','showTitle');
}
searchGui.showTitle=showTitle;
//hidden result 'show' link
let showLink=document.createElement('a');
with(showLink){
href='javascript:void(0);';
setAttribute('class','showLink');
}
searchGui.showLink=showLink;
},
showResult: function(node,contentNodes,showTitle,showLink){
/**
re-show hidden filtered result
*/
if(searchGui.resHidden){
node.style.display='';
}else{
for(let i=0;i<contentNodes.length;i++){
contentNodes[i].style.display='';
}
showTitle.style.display='none';
showLink.innerHTML='hide';
let hideListener=function(e){
searchGui.hideResult(node,null,null,contentNodes,showTitle,showLink);
e.preventDefault();
this.removeEventListener('click',hideListener,false);
};
showLink.addEventListener('click',hideListener,false);
}
},
hideResult: function(node,filter,nodeData,contentNodes,showTitle,showLink){
/**
hide filtered result
args:
contentNodes (optional): content nodes that will be hidden
showTitle (optional): title that will be shown
showLink (optional): link that will be shown, changed to 'hide'
*/
//hide node
if(searchGui.resHidden){
node.style.display='none';
}else{
//show only title and 'show' link
if(filter){
contentNodes=[];
for(let i=0;i<node.childNodes.length;i++){
let childNode=node.childNodes[i];
if(childNode.style){
childNode.style.display='none';
contentNodes.push(childNode);
}
}
showTitle=searchGui.showTitle.cloneNode(false);
let title=nodeData.title;
if(title){
showTitle.innerHTML=title+' ';
}
node.appendChild(showTitle);
showLink=searchGui.showLink.cloneNode(false);
showLink.innerHTML='show';
showLink.title=filter.fullText||filter.text;
node.appendChild(showLink);
}else{
for(let i=0;i<contentNodes.length;i++){
contentNodes[i].style.display='none';
}
showTitle.style.display='';
showLink.innerHTML='show';
}
let showListener=function(e){
searchGui.showResult(node,contentNodes,showTitle,showLink);
e.preventDefault();
this.removeEventListener('click',showListener,false);
};
showLink.addEventListener('click',showListener,false);
}
return true;
},
createAddLink: function(node,nodeData){
let linkArea=nodeData.linkArea;
if(!linkArea) return;
linkArea.appendChild(searchGui.dash.cloneNode(true));
let addLink=searchGui.addLink.cloneNode(true);
linkArea.appendChild(addLink);
let addListener=function(e){
searchGui.addFromResult(nodeData);
}
addLink.addEventListener('click',addListener,false);
},
removeAddLink: function(nodeData){
let linkArea=nodeData.linkArea;
if(!linkArea) return;
linkArea.removeChild(linkArea.lastChild);
linkArea.removeChild(linkArea.lastChild);
},
addFromResult: function(nodeData){
//trim domainUrl
let domainUrl='||'+nodeData.url.replace(/^[\w\-]+:\/+(?:www\.)?/,'');
let text=prompt('Filter: ',domainUrl);
if(text==null) return;
let keys=gfpFilter.getKeys(text);
if(gfpFilter.isDuplicate(keys,Filter.knownFilters)){
alert('Filter already exists');
return;
}
//add filter
let filter=gfpFilter.fromTextCompiled(text,Filter.knownFilters);
gfpMatcher.add(filter,keys);
prefMeta.isUpdated=false;
searchGui.filterResultsRem();
},
_filterResultsRem: function(remNode){
/**
returns: if node is hidden
*/
let node,remNodes,filterClass;
if(remNode.constructor==Array){
//check if parent node needs to be hidden
remNodes=remNode;
remNode=remNodes[0];
}
node=remNode.node;
filterClass=remNode.filterClass;
let _nodeData=new searchGui.nodeData(node,filterClass);
let filter=defaultMatcher.matchesAny(_nodeData);
if(filter){
filter.hitCount++;
if(!(filter instanceof WhitelistFilter)){
searchGui.removeAddLink(_nodeData);
searchGui.hideResult(node,filter,_nodeData);
return true;
}else{
return false;
}
}
if(remNodes){
//hide child nodes
let allHidden=true;
for(let i=1;i<remNodes.length;i++){
let res=searchGui._filterResultsRem(remNodes[i]);
if(res){
remNodes.splice(i,1);
i--;
}else{
allHidden=false;
}
}
if(allHidden){
if(searchGui.resHidden){
//hide parent node
searchGui.hideResult(node,null,_nodeData);
}
}
return allHidden;
}else{
return false;
}
},
_filterResults: function(node,filterClass){
/**
returns: remNode
*/
let filterClass=searchGui.getResultType(node,filterClass);
if(filterClass==null){
//unkown node type
return null;
}
let _nodeData=new searchGui.nodeData(node,filterClass);
let filter=defaultMatcher.matchesAny(_nodeData);
if(filter){
filter.hitCount++;
if(!(filter instanceof WhitelistFilter)){
searchGui.hideResult(node,filter,_nodeData);
}
return null;
}
searchGui.createAddLink(node,_nodeData);
let resNodes=filterClass.getResults(node);
if(!resNodes){
return new searchGui.remNode(node,filterClass);
}
//filter subnodes
let remNodes=[new searchGui.remNode(node,filterClass)];
for(let i=0;i<resNodes.length;i++){
let _node=resNodes[i];
let res=searchGui._filterResults(_node,filterClass);
if(res){
remNodes.push(res);
}
}
if(remNodes.length==1){
if(searchGui.resHidden){
//hide parent node
searchGui.hideResult(node,null,_nodeData);
}
return null;
}
return remNodes;
},
filterResultsRem: function(){
searchGui._filterResultsRem(searchGui.remNodes);
gfpFilter.save();
},
filterResults: function(){
searchGui.remNodes=searchGui._filterResults(searchGui.getResults());
gfpFilter.save();
},
init: function(){
let resHidden=GM_getValue('resHidden');
if(resHidden==undefined){
resHidden=config.resHidden;
GM_setValue('resHidden',resHidden);
}
searchGui.resHidden=resHidden;
searchGui.addStyles();
searchGui.initNodes();
searchGui.initialized=true;
},
}
let prefLink={
createLink: function(){
let linkEx;
let link=document.createElement('a');
if(!linkEx){
linkEx=document.querySelector('a.gbmt[href^="/preferences?"]');
if(linkEx){
let linkExParent=linkEx.parentNode;
let linkParent=linkExParent.cloneNode(false);
linkParent.setAttribute('class',linkExParent.getAttribute('class'));
linkParent.appendChild(link);
linkExParent.parentNode.insertBefore(linkParent,linkExParent.nextElementSibling);
}
}
if(!linkEx)
return null;
link.setAttribute('class',linkEx.getAttribute('class'));
link.setAttribute('style',linkEx.getAttribute('style'));
link.setAttribute('href','javascript:void(0);');
link.appendChild(document.createTextNode('Config Filters'));
return link;
},
init: function(){
let link=prefLink.createLink();
if(link==null) return false;
link.addEventListener('click',function(){
if(prefMeta.isUpdated){
pref.show();
}else{
pref.renderAll();
prefMeta.isUpdated=true;
}
},false);
},
}
let ext={
gmonkeyr: {
loaded: function(){
let footAnim=_$('navcnt');
if(!footAnim) return false;
footAnim=footAnim.firstElementChild;
if(footAnim.nodeName=='DIV' && footAnim.textContent=='Loading'){
let results=searchGui.getResults();
let resultsListener=function(e){
let node=e.target;
switch(node.nodeName){
case 'HR':
results.removeEventListener('DOMNodeInserted',resultsListener,false);
return;
case 'LI':
break;
default:
return;
}
let res=searchGui._filterResults(node,searchGui.r.res);
if(res){
searchGui.remNodes.push(res);
}else{
prefMeta.isUpdated=false;
}
};
let defaultDisp=footAnim.style.display;
//add resultsListener only after loading image hides, so no additional events fire when page loads
footAnim.addEventListener('DOMAttrModified',function(e){
if(footAnim.style.display==defaultDisp){
results.addEventListener('DOMNodeInserted',resultsListener,false);
if(!prefMeta.isUpdated){
gfpFilter.save();
}
}
},false);
}
},
},
customSearch: {
init: function(){
//check if url is a google custom search url
let location=window.location.href;
if(location.indexOf('/cse?')==-1 && location.indexOf('/custom?')==-1){
return;
}
//google custom search layout
prefLink.createLink=function(){
let linkParent=searchGui.getResults();
let link=document.createElement('a');
link.setAttribute('style','float:right; margin-top:10px; color:#0000CC; font-size:14px; text-decoration:none;');
link.setAttribute('href','javascript:void(0);');
link.appendChild(document.createTextNode('Config Filters'));
linkParent.insertBefore(document.createElement('br'),linkParent.firstElementChild);
linkParent.insertBefore(link,linkParent.firstElementChild);
return link;
}
searchGui.r={
res: {
getResults: function(node) node.querySelectorAll('div.g'),
getLinkArea: function() null,
getUrl: function() null,
getTitle: function() null,
getSummary: function() null,
},
text: {
getResults: function() null,
getLinkArea: function(node) node.querySelector('div.std'),
getUrl: function(node) node.querySelector('a.l').href,
getTitle: function(node) (node.querySelector('h2.r')||node.querySelector('h3.r')).textContent,
getSummary: function(node) node.querySelector('div#std').textContent,
},
}
searchGui.getResultType=function(node,filterClass){
if(node.id=='res'){
return searchGui.r.res;
}else if(node.getAttribute('class')=='g'){
return searchGui.r.text;
}
}
//use direct links
let links=searchGui.getResults().querySelectorAll('div.g>a.l');
for(let i=0;i<links.length;i++){
let link=links[i];
link.href=searchGui.directLink(link.getAttribute('href'));
}
},
},
instant: {
init: function(){
//last hidden node
let lastHideNode;
//last processed res node
let lastProcNode;
//if results are completely new
let isNew=false;
//all results loaded
let isLoaded=false;
//results parent node
let resultsNode;
function filterNodes(currNode,isNew){
if(!searchGui.initialized){
searchGui.init();
}
if(!isNew){
currNode=currNode.nextElementSibling;
}
let lastNode;
while(currNode){
let res=searchGui._filterResults(currNode,searchGui.r.res);
if(res){
searchGui.remNodes.push(res);
}else{
prefMeta.isUpdated=false;
}
currNode.style.display='';
lastNode=currNode;
currNode=currNode.nextElementSibling;
}
return lastNode;
}
function isInDom(node){
return node.parentNode && node.parentNode.offsetWidth!=0;
}
//hook XMLHttpRequest to get catch google results
let XMLHttpRequest=unsafeWindow.XMLHttpRequest;
XMLHttpRequest.prototype.__defineSetter__('onreadystatechange',function(func){
if(this.listener){
this.removeEventListener('readystatechange',this.listener,false);
}
this.o_listener=func;
this.listener=function(){
func.call(this,arguments);
let currNode;
if(lastHideNode && isInDom(lastHideNode)){
currNode=lastHideNode.nextElementSibling;
if(currNode){
isNew=false;
}else{
return;
}
}else{
currNode=$X('//ol[@id="rso"]/li',9);
if(currNode){
searchGui.remNodes=[new searchGui.remNode(currNode.parentNode,searchGui.r.res)];
lastProcNode=currNode;
isNew=true;
}else{
return;
}
}
while(currNode){
currNode.style.display='none';
lastHideNode=currNode;
currNode=currNode.nextElementSibling;
}
isLoaded=(this.readyState==4);
window.postMessage('xhr','*');
};
this.addEventListener('readystatechange',this.listener,false);
});
let o_open=XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open=function(method,url){
if(url.substr(0,3)!='/s?'){
if(this.listener){
this.removeEventListener('readystatechange',this.listener,false);
this.addEventListener('readystatechange',this.o_listener,false);
}
}
return o_open.apply(this,arguments);
}
//listen to xhr node hide requests
window.addEventListener('message',function(e){
if(e.data!='xhr') return;
//safety check
if(!lastProcNode) return;
lastProcNode=filterNodes(lastProcNode,isNew);
if(isLoaded){
if(!prefMeta.isUpdated){
gfpFilter.save();
}
isLoaded=false;
}
},false);
//listen to cached requests
unsafeWindow.addEventListener('message',function(e){
if(e.data!='comm.df') return;
//if already filtered
if(lastProcNode && isInDom(lastProcNode)) return;
resultsNode.style.display='';
let currNode=$X('//ol[@id="rso"]/li',9);
if(currNode){
searchGui.remNodes=[new searchGui.remNode(currNode.parentNode,searchGui.r.res)];
lastProcNode=filterNodes(currNode,true);
if(!prefMeta.isUpdated){
gfpFilter.save();
}
isLoaded=false;
}
},false);
},
},
init: function(){
for(let i=0;i<config.ext.length;i++){
let e=ext[config.ext[i]];
if('init' in e) e.init();
}
},
loaded: function(){
for(let i=0;i<config.ext.length;i++){
let e=ext[config.ext[i]];
if('loaded' in e) e.loaded();
}
},
}
function init(){
logTime.start();
if(ext.init()==false) return;
if(gfpFilter.init()==false) return;
if(gfpMatcher.init()==false) return;
if(prefLink.init()==false) return;
if(searchGui.isHomePage()) return;
if(searchGui.init()==false) return;
if(searchGui.filterResults()==false) return;
if(ext.loaded()==false) return;
logTime.end('Total init time');
}
/**
editable grid
*/
/*
* EditableGrid.js
*
* Copyright 2010 Webismymind SPRL
*
* This file is part of EditableGrid.
*
* EditableGrid 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
* any later version.
*
* EditableGrid 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 EditableGrid. If not, see http://www.gnu.org/licenses.
*
*/
if (typeof _$ == 'undefined') {
function _$(elementId) { return document.getElementById(elementId); }
}
/**
* Creates a new column
* @constructor
* @class Represents a column in the editable grid
* @param {Object} config
*/
function Column(config)
{
// default properties
var props = {
name: "",
label: "",
editable: true,
renderable: true,
datatype: "string",
unit: null,
precision: null,
nansymbol: '',
bar: true, // is the column to be displayed in a bar chart ? relevant only for numerical columns
headerRenderer: null,
headerEditor: null,
cellRenderer: null,
cellEditor: null,
cellValidators: [],
enumProvider: null,
optionValues: null,
columnIndex: -1
};
// override default properties with the ones given
for (var p in props) this[p] = (typeof config == 'undefined' || typeof config[p] == 'undefined') ? props[p] : config[p];
}
Column.prototype.getOptionValuesForRender = function(rowIndex) {
var values = this.enumProvider.getOptionValuesForRender(this.editablegrid, this, rowIndex);
return values ? values : this.optionValues;
};
Column.prototype.getOptionValuesForEdit = function(rowIndex) {
var values = this.enumProvider.getOptionValuesForEdit(this.editablegrid, this, rowIndex);
return values ? values : this.optionValues;
};
Column.prototype.isValid = function(value) {
for (var i = 0; i < this.cellValidators.length; i++) if (!this.cellValidators[i].isValid(value)) return false;
return true;
};
Column.prototype.isNumerical = function() {
return this.datatype =='double' || this.datatype =='integer';
};
/**
* Creates a new enumeration provider
* @constructor
* @class Base class for all enumeration providers
* @param {Object} config
*/
function EnumProvider(config)
{
// default properties
this.getOptionValuesForRender = function(grid, column, rowIndex) { return null; };
this.getOptionValuesForEdit = function(grid, column, rowIndex) { return null; };
// override default properties with the ones given
for (var p in config) this[p] = config[p];
}
/**
* Creates a new EditableGrid.
* <p>You can specify here some configuration options (optional).
* <br/>You can also set these same configuration options afterwards.
* <p>These options are:
* <ul>
* <li>enableSort: enable sorting when clicking on column headers (default=true)</li>
* <li>doubleclick: use double click to edit cells (default=false)</li>
* <li>editmode: can be one of
* <ul>
* <li>absolute: cell editor comes over the cell (default)</li>
* <li>static: cell editor comes inside the cell</li>
* <li>fixed: cell editor comes in an external div</li>
* </ul>
* </li>
* <li>editorzoneid: used only when editmode is set to fixed, it is the id of the div to use for cell editors</li>
* <li>allowSimultaneousEdition: tells if several cells can be edited at the same time (default=false)<br/>
* Warning: on some Linux browsers (eg. Epiphany), a blur event is sent when the user clicks on a 'select' input to expand it.
* So practically, in these browsers you should set allowSimultaneousEdition to true if you want to use columns with option values and/or enum providers.
* This also used to happen in older versions of Google Chrome Linux but it has been fixed, so upgrade if needed.</li>
* <li>invalidClassName: CSS class to apply to text fields when the entered value is invalid (default="invalid")</li>
* <li>ignoreLastRow: ignore last row when sorting and charting the data (typically for a 'total' row)</li>
* <li>caption: text to use as the grid's caption</li>
* </ul>
* @constructor
* @class EditableGrid
*/
function EditableGrid(name, config)
{
if (typeof name != "string" || typeof config != "object") {
alert("The EditableGrid constructor takes two arguments:\n- name (string)\n- config (object)\n\nGot instead " + (typeof name) + " and " + (typeof config) + ".");
};
// default properties
var props =
{
enableSort: true,
doubleclick: false,
editmode: "absolute",
editorzoneid: "",
allowSimultaneousEdition: false,
saveOnBlur: true,
invalidClassName: "invalid",
ignoreLastRow: false, // ignore last row for sorting and pie/bar charts
caption: null
};
// override default properties with the ones given
for (var p in props) this[p] = props[p];
for (var p in config) this[p] = config[p];
this.Browser = {
IE: !!(window.attachEvent && navigator.userAgent.indexOf('Opera') === -1),
Opera: navigator.userAgent.indexOf('Opera') > -1,
WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
Gecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') === -1,
MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)
};
// private data
this.name = name;
this.columns = [];
this.data = [];
this.xmlDoc = null;
this.sortedColumnName = -1;
this.sortDescending = false;
this.baseUrl = this.detectDir();
this.nbHeaderRows = 1;
this.lastSelectedRowIndex = -1;
if (this.enableSort) {
this.sortUpImage = new Image();
this.sortUpImage.src = this.baseUrl + "/images/bullet_arrow_up.png";
this.sortDownImage = new Image();
this.sortDownImage.src = this.baseUrl + "/images/bullet_arrow_down.png";
}
var _this=this;
//sets new mouseClickedWrapper for each individual grid object
if(!this.mouseClickedWrapper) this.mouseClickedWrapper=function(e){ _this.mouseClicked(e); };
}
/**
* Callback functions
*/
EditableGrid.prototype.tableLoaded = function() {};
EditableGrid.prototype.chartRendered = function() {};
EditableGrid.prototype.tableRendered = function(containerid, className, tableid) {};
EditableGrid.prototype.tableSorted = function() {};
EditableGrid.prototype.tableFiltered = function() {};
EditableGrid.prototype.modelChanged = function(rowIndex, columnIndex, oldValue, newValue, row) {};
EditableGrid.prototype.rowSelected = function(oldRowIndex, newRowIndex) {};
EditableGrid.prototype.isHeaderEditable = function(rowIndex, columnIndex) { return false; };
EditableGrid.prototype.isEditable =function(rowIndex, columnIndex) { return true; };
EditableGrid.prototype.readonlyWarning = function() {};
/**
* Load metadata and data from an XML url
*/
EditableGrid.prototype.loadXML = function(url)
{
// we use a trick to avoid getting an old version from the browser's cache
var orig_url = url;
var sep = url.indexOf('?') >= 0 ? '&' : '?';
url += sep + Math.floor(Math.random() * 100000);
with (this) {
// IE
if (window.ActiveXObject)
{
xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
xmlDoc.addEventListener('readystatechange', function() {
if (xmlDoc.readyState == 4) {
processXML();
tableLoaded();
}
}, false);
xmlDoc.load(url);
}
// Safari
else if (/*Browser.WebKit && */ window.XMLHttpRequest)
{
xmlDoc = new XMLHttpRequest();
xmlDoc.addEventListener('readystatechange', function () {
if (xmlDoc.readyState == 4) {
xmlDoc = xmlDoc.responseXML;
if (!xmlDoc) { /* alert("Could not load XML from url '" + orig_url + "'"); */ return false; }
processXML();
tableLoaded();
}
}, false);
xmlDoc.open("GET", url, true);
xmlDoc.send("");
}
// Firefox (and other browsers)
else if (document.implementation && document.implementation.createDocument)
{
xmlDoc = document.implementation.createDocument("", "", null);
xmlDoc.addEventListener('load', function() {
processXML();
tableLoaded();
}, false);
xmlDoc.load(url);
}
// should never happen
else {
alert("Cannot load XML file with this browser!");
return false;
}
return true;
}
};
/**
* Process the XML content
* @private
*/
EditableGrid.prototype.processXML = function()
{
with (this) {
// clear model and pointer to current table
this.columns = [];
this.data = [];
this.table = null;
// load metadata (only one tag <metadata> --> metadata[0])
var metadata = xmlDoc.getElementsByTagName("metadata");
if (!metadata || metadata.length < 1) return false;
var columnDeclarations = metadata[0].getElementsByTagName("column");
for (var i = 0; i < columnDeclarations.length; i++) {
// get column type
var col = columnDeclarations[i];
var datatype = col.getAttribute("datatype");
// get enumerated values if any
var optionValues = null;
var enumValues = col.getElementsByTagName("values");
if (enumValues.length > 0) {
optionValues = {};
enumValues = enumValues[0].getElementsByTagName("value");
for (var v = 0; v < enumValues.length; v++) {
optionValues[enumValues[v].getAttribute("value")] = enumValues[v].firstChild ? enumValues[v].firstChild.nodeValue : "";
}
}
// create new column
var column = new Column({
name: col.getAttribute("name"),
label: typeof col.getAttribute("label") == 'string' ? col.getAttribute("label") : col.getAttribute("name"),
datatype: col.getAttribute("datatype") ? col.getAttribute("datatype") : "string",
editable : col.getAttribute("editable") == "true",
bar : col.getAttribute("bar") ? col.getAttribute("bar") == "true" : true,
optionValues: optionValues,
enumProvider: (optionValues ? new EnumProvider() : null),
columnIndex: i
});
// parse column type
parseColumnType(column);
// create suited cell renderer
_createCellRenderer(column);
_createHeaderRenderer(column);
// create suited cell editor
_createCellEditor(column);
_createHeaderEditor(column);
// add default cell validators based on the column type
_addDefaultCellValidators(column);
// add column
column.editablegrid = this;
columns.push(column);
}
// load content
var rows = xmlDoc.getElementsByTagName("row");
for (var i = 0; i < rows.length; i++)
{
// get all defined cell values
var cellValues = {};
var cols = rows[i].getElementsByTagName("column");
for (var j = 0; j < cols.length; j++) {
var colname = cols[j].getAttribute("name");
if (!colname) {
if (j >= columns.length) alert("You defined too many columns for row " + (i+1));
else colname = columns[j].name;
}
cellValues[colname] = cols[j].firstChild ? cols[j].firstChild.nodeValue : "";
}
// for each row we keep the orginal index, the id and all other attributes that may have been set in the XML
var rowData = { originalIndex: i, id: rows[i].getAttribute("id") ? rows[i].getAttribute("id") : "" };
for (var attrIndex = 0; attrIndex < rows[i].attributes.length; attrIndex++) {
var node = rows[i].attributes.item(attrIndex);
if (node.nodeName != "id") rowData[node.nodeName] = node.nodeValue;
}
// get column values for this rows
rowData.columns = [];
for (var c = 0; c < columns.length; c++) {
var cellValue = columns[c].name in cellValues ? cellValues[columns[c].name] : "";
rowData.columns.push(getTypedValue(c, cellValue));
}
// add row data in our model
data.push(rowData);
}
}
};
/**
* Parse column type
* @private
*/
EditableGrid.prototype.parseColumnType = function(column)
{
// extract precision, unit and nansymbol from type if two given
if (column.datatype.match(/(.*)\((.*),(.*),(.*)\)$/)) {
column.datatype = RegExp.$1;
column.unit = RegExp.$2.trim();
column.precision = parseInt(RegExp.$3);
column.nansymbol = RegExp.$4.trim();
}
// extract precision and unit from type if two given
if (column.datatype.match(/(.*)\((.*),(.*)\)$/)) {
column.datatype = RegExp.$1.trim();
column.unit = RegExp.$2.trim();
column.precision = parseInt(RegExp.$3);
}
// extract precision or unit from type if any given
if (column.datatype.match(/(.*)\((.*)\)$/)) {
column.datatype = RegExp.$1.trim();
var unit_or_precision = RegExp.$2.trim();
if (unit_or_precision.match(/^[0-9]*$/)) column.precision = parseInt(unit_or_precision);
else column.unit = unit_or_precision;
}
if (isNaN(column.precision)) column.precision = null;
if (column.unit == '') column.unit = null;
if (column.nansymbol == '') column.nansymbol = null;
};
/**
* Get typed value
* @private
*/
EditableGrid.prototype.getTypedValue = function(columnIndex, cellValue)
{
var colType = this.getColumnType(columnIndex);
if (colType == 'boolean') cellValue = (cellValue && cellValue != 0 && cellValue != "false") ? true : false;
if (colType == 'integer') { cellValue = parseInt(cellValue); }
if (colType == 'double') { cellValue = parseFloat(cellValue); }
if (colType == 'string') { cellValue = "" + cellValue; }
return cellValue;
};
/**
* Attach to an existing HTML table, using given column definitions
*/
EditableGrid.prototype.attachToHTMLTable = function(_table, _columns)
{
with (this) {
// we have our new columns
columns = _columns;
for (var c = 0; c < columns.length; c++) {
// set column index and back pointer
var column = columns[c];
column.editablegrid = this;
column.columnIndex = c;
// parse column type
parseColumnType(column);
// create suited enum provider, renderer and editor if none given
if (!column.enumProvider) column.enumProvider = column.optionValues ? new EnumProvider() : null;
if (!column.cellRenderer) _createCellRenderer(column);
if (!column.headerRenderer) _createHeaderRenderer(column);
if (!column.cellEditor) _createCellEditor(column);
if (!column.headerEditor) _createHeaderEditor(column);
// add default cell validators based on the column type
_addDefaultCellValidators(column);
}
// get pointers to table components
this.table = _table;
this.tHead = _table.tHead;
this.tBody = _table.tBodies[0];
// create table body if needed
if (!tBody) {
tBody = document.createElement("TBODY");
table.insertBefore(tBody, table.firstChild);
}
// create table header if needed
if (!tHead) {
tHead = document.createElement("THEAD");
table.insertBefore(tHead, tBody);
}
// if header is empty use first body row as header
if (tHead.rows.length == 0 && tBody.rows.length > 0)
tHead.appendChild(tBody.rows[0]);
// check that header has exactly one row
this.nbHeaderRows = tHead.rows.length;
/*if (tHead.rows.length != 1) {
alert("You table header must have exactly row!");
return false;
}*/
// load header labels
var rows = tHead.rows;
for (var i = 0; i < rows.length; i++) {
var cols = rows[i].cells;
var columnIndexInModel = 0;
for (var j = 0; j < cols.length && columnIndexInModel < columns.length; j++) {
if (!columns[columnIndexInModel].label) columns[columnIndexInModel].label = cols[j].innerHTML;
var colspan = parseInt(cols[j].getAttribute("colspan"));
columnIndexInModel += colspan > 1 ? colspan : 1;
}
}
// load content
var rows = tBody.rows;
for (var i = 0; i < rows.length; i++) {
var rowData = [];
var cols = rows[i].cells;
for (var j = 0; j < cols.length && j < columns.length; j++) rowData.push(this.getTypedValue(j, cols[j].innerHTML));
data.push({originalIndex: i, id: rows[i].id, columns: rowData});
rows[i].id = this.name + '_' + rows[i].id;
}
}
};
/**
* Creates a suitable cell renderer for the column
* @private
*/
EditableGrid.prototype._createCellRenderer = function(column)
{
column.cellRenderer =
column.enumProvider ? new EnumCellRenderer() :
column.datatype == "integer" || column.datatype == "double" ? new NumberCellRenderer() :
column.datatype == "boolean" ? new CheckboxCellRenderer() :
column.datatype == "email" ? new EmailCellRenderer() :
column.datatype == "website" || column.datatype == "url" ? new WebsiteCellRenderer() :
column.datatype == "date" ? new DateCellRenderer() :
new CellRenderer();
// give access to the column from the cell renderer
if (column.cellRenderer) {
column.cellRenderer.editablegrid = this;
column.cellRenderer.column = column;
}
};
/**
* Creates a suitable header cell renderer for the column
* @private
*/
EditableGrid.prototype._createHeaderRenderer = function(column)
{
column.headerRenderer = (this.enableSort && column.datatype != "html") ? new SortHeaderRenderer(column.name) : new CellRenderer();
// give access to the column from the header cell renderer
if (column.headerRenderer) {
column.headerRenderer.editablegrid = this;
column.headerRenderer.column = column;
}
};
/**
* Creates a suitable cell editor for the column
* @private
*/
EditableGrid.prototype._createCellEditor = function(column)
{
column.cellEditor =
column.enumProvider ? new SelectCellEditor() :
column.datatype == "integer" || column.datatype == "double" ? new NumberCellEditor(column.datatype) :
column.datatype == "boolean" ? null :
column.datatype == "email" ? new TextCellEditor(column.precision) :
column.datatype == "website" || column.datatype == "url" ? new TextCellEditor(column.precision) :
column.datatype == "date" ? new TextCellEditor(column.precision, 10) :
new TextCellEditor(column.precision);
// give access to the column from the cell editor
if (column.cellEditor) {
column.cellEditor.editablegrid = this;
column.cellEditor.column = column;
}
};
/**
* Creates a suitable header cell editor for the column
* @private
*/
EditableGrid.prototype._createHeaderEditor = function(column)
{
column.headerEditor = new TextCellEditor();
// give access to the column from the cell editor
if (column.headerEditor) {
column.headerEditor.editablegrid = this;
column.headerEditor.column = column;
}
};
/**
* Returns the number of rows
*/
EditableGrid.prototype.getRowCount = function()
{
return this.data.length;
};
/**
* Returns the number of columns
*/
EditableGrid.prototype.getColumnCount = function()
{
return this.columns.length;
};
/**
* Returns the column
* @param {Object} columnIndexOrName index or name of the column
*/
EditableGrid.prototype.getColumn = function(columnIndexOrName)
{
var colIndex = this.getColumnIndex(columnIndexOrName);
if (colIndex < 0) { alert("[getColumn] Column not found with index or name " + columnIndexOrName); return null; }
return this.columns[colIndex];
};
/**
* Returns the name of a column
* @param {Object} columnIndexOrName index or name of the column
*/
EditableGrid.prototype.getColumnName = function(columnIndexOrName)
{
return this.getColumn(columnIndexOrName).name;
};
/**
* Returns the label of a column
* @param {Object} columnIndexOrName index or name of the column
*/
EditableGrid.prototype.getColumnLabel = function(columnIndexOrName)
{
return this.getColumn(columnIndexOrName).label;
};
/**
* Returns the type of a column
* @param {Object} columnIndexOrName index or name of the column
*/
EditableGrid.prototype.getColumnType = function(columnIndexOrName)
{
return this.getColumn(columnIndexOrName).datatype;
};
/**
* Returns the unit of a column
* @param {Object} columnIndexOrName index or name of the column
*/
EditableGrid.prototype.getColumnUnit = function(columnIndexOrName)
{
return this.getColumn(columnIndexOrName).unit;
};
/**
* Returns the precision of a column
* @param {Object} columnIndexOrName index or name of the column
*/
EditableGrid.prototype.getColumnPrecision = function(columnIndexOrName)
{
return this.getColumn(columnIndexOrName).precision;
};
/**
* Returns true if the column is to be displayed in a bar chart
* @param {Object} columnIndexOrName index or name of the column
*/
EditableGrid.prototype.isColumnBar = function(columnIndexOrName)
{
var column = this.getColumn(columnIndexOrName);
return (column.bar && column.isNumerical());
};
/**
* Returns true if the column is numerical (double or integer)
* @param {Object} columnIndexOrName index or name of the column
*/
EditableGrid.prototype.isColumnNumerical = function(columnIndexOrName)
{
var column = this.getColumn(columnIndexOrName);
return column.isNumerical();;
};
/**
* Returns the value at the specified index
* @param {Integer} rowIndex
* @param {Integer} columnIndex
*/
EditableGrid.prototype.getValueAt = function(rowIndex, columnIndex)
{
// check and get column
if (columnIndex < 0 || columnIndex >= this.columns.length) { alert("[getValueAt] Invalid column index " + columnIndex); return null; }
var column = this.columns[columnIndex];
// get value in model
if (rowIndex < 0) return column.label;
var rowData = this.data[rowIndex]['columns'];
return rowData ? rowData[columnIndex] : null;
};
/**
* Sets the value at the specified index
* @param {Integer} rowIndex
* @param {Integer} columnIndex
* @param {Object} value
* @param {Boolean} render
*/
EditableGrid.prototype.setValueAt = function(rowIndex, columnIndex, value, render)
{
if (typeof render == "undefined") render = true;
var previousValue = null;;
// check and get column
if (columnIndex < 0 || columnIndex >= this.columns.length) { alert("[setValueAt] Invalid column index " + columnIndex); return null; }
var column = this.columns[columnIndex];
// set new value in model
if (rowIndex < 0) {
previousValue = column.label;
column.label = value;
}
else {
var rowData = this.data[rowIndex]['columns'];
previousValue = rowData[columnIndex];
if (rowData) rowData[columnIndex] = this.getTypedValue(columnIndex, value);
}
// render new value
if (render) {
var renderer = rowIndex < 0 ? column.headerRenderer : column.cellRenderer;
renderer._render(rowIndex, columnIndex, this.getCell(rowIndex, columnIndex), value);
}
return previousValue;
};
/**
* Find column index from its name
* @param {Object} columnIndexOrName index or name of the column
*/
EditableGrid.prototype.getColumnIndex = function(columnIndexOrName)
{
if (typeof columnIndexOrName == "undefined" || columnIndexOrName === "") return -1;
// TODO: problem because the name of a column could be a valid index, and we cannot make the distinction here!
// if columnIndexOrName is a number which is a valid index return it
if (!isNaN(columnIndexOrName) && columnIndexOrName >= 0 && columnIndexOrName < this.columns.length) return columnIndexOrName;
// otherwise search for the name
for (var c = 0; c < this.columns.length; c++) if (this.columns[c].name == columnIndexOrName) return c;
return -1;
};
/**
* Get row object at given index
* @param {Integer} index of the row
*/
EditableGrid.prototype.getRow = function(rowIndex)
{
if (rowIndex < 0) return this.tHead.rows[rowIndex + this.nbHeaderRows];
return this.tBody.rows[rowIndex];
};
/**
* Get row id specified in XML or HTML
* @param {Integer} index of the row
*/
EditableGrid.prototype.getRowId = function(rowIndex)
{
return (rowIndex < 0 || rowIndex >= this.data.length) ? null : this.data[rowIndex]['id'];
};
/**
* Get custom row attribute specified in XML
* @param {Integer} index of the row
* @param {String} name of the attribute
*/
EditableGrid.prototype.getRowAttribute = function(rowIndex, attributeName)
{
return this.data[rowIndex][attributeName];
};
/**
* Set custom row attribute
* @param {Integer} index of the row
* @param {String} name of the attribute
* @param value of the attribute
*/
EditableGrid.prototype.setRowAttribute = function(rowIndex, attributeName, attributeValue)
{
this.data[rowIndex][attributeName] = attributeValue;
};
/**
* Remove row with given id
* @param {Integer} rowId
*/
EditableGrid.prototype.removeRow = function(rowId)
{
var tr = _$(this.name + "_" + rowId);
var rowIndex = tr.rowIndex - this.nbHeaderRows; // remove header rows
this.tBody.removeChild(tr);
this.data.splice(rowIndex, 1);
};
/**
* Get index of row with given id
* @param {Integer} rowId or row object
*/
EditableGrid.prototype.getRowIndex = function(rowId)
{
var tr = typeof rowId == 'object' ? rowId : _$(this.name + "_" + rowId);
return tr ? tr.rowIndex - this.nbHeaderRows : -1; // remove header rows
};
/**
* Add row with given id and data
* @param {Integer} rowId
* @param {Integer} columns
* @param {Boolean} dontSort
*/
EditableGrid.prototype.addRow = function(rowId, cellValues, dontSort)
{
with (this) {
// add row in data
var rowData = [];
for (var c = 0; c < columns.length; c++) {
var cellValue = columns[c].name in cellValues ? cellValues[columns[c].name] : "";
rowData.push(getTypedValue(c, cellValue));
}
var rowIndex = data.length;
data.push({originalIndex: rowIndex, id: rowId, columns: rowData});
// create row in table and render content
var tr = tBody.insertRow(rowIndex);
tr.id = this.name + "_" + rowId;
for (var c = 0; c < columns.length; c++) {
var td = tr.insertCell(c);
columns[c].cellRenderer._render(rowIndex, c, td, getValueAt(rowIndex,c));
}
// resort table
if (!dontSort) sort();
}
};
/**
* Sets the column header cell renderer for the specified column index
* @param {Object} columnIndexOrName index or name of the column
* @param {CellRenderer} cellRenderer
*/
EditableGrid.prototype.setHeaderRenderer = function(columnIndexOrName, cellRenderer)
{
var columnIndex = this.getColumnIndex(columnIndexOrName);
if (columnIndex < 0) alert("[setHedareRenderer] Invalid column: " + columnIndexOrName);
else {
var column = this.columns[columnIndex];
column.headerRenderer = (this.enableSort && column.datatype != "html") ? new SortHeaderRenderer(column.name, cellRenderer) : cellRenderer;
// give access to the column from the cell renderer
if (cellRenderer) {
if (this.enableSort && column.datatype != "html") {
column.headerRenderer.editablegrid = this;
column.headerRenderer.column = column;
}
cellRenderer.editablegrid = this;
cellRenderer.column = column;
}
}
};
/**
* Sets the cell renderer for the specified column index
* @param {Object} columnIndexOrName index or name of the column
* @param {CellRenderer} cellRenderer
*/
EditableGrid.prototype.setCellRenderer = function(columnIndexOrName, cellRenderer)
{
var columnIndex = this.getColumnIndex(columnIndexOrName);
if (columnIndex < 0) alert("[setCellRenderer] Invalid column: " + columnIndexOrName);
else {
var column = this.columns[columnIndex];
column.cellRenderer = cellRenderer;
// give access to the column from the cell renderer
if (cellRenderer) {
cellRenderer.editablegrid = this;
cellRenderer.column = column;
}
}
};
/**
* Sets the cell editor for the specified column index
* @param {Object} columnIndexOrName index or name of the column
* @param {CellEditor} cellEditor
*/
EditableGrid.prototype.setCellEditor = function(columnIndexOrName, cellEditor)
{
var columnIndex = this.getColumnIndex(columnIndexOrName);
if (columnIndex < 0) alert("[setCellEditor] Invalid column: " + columnIndexOrName);
else {
var column = this.columns[columnIndex];
column.cellEditor = cellEditor;
// give access to the column from the cell editor
if (cellEditor) {
cellEditor.editablegrid = this;
cellEditor.column = column;
}
}
};
/**
* Sets the header cell editor for the specified column index
* @param {Object} columnIndexOrName index or name of the column
* @param {CellEditor} cellEditor
*/
EditableGrid.prototype.setHeaderEditor = function(columnIndexOrName, cellEditor)
{
var columnIndex = this.getColumnIndex(columnIndexOrName);
if (columnIndex < 0) alert("[setHeaderEditor] Invalid column: " + columnIndexOrName);
else {
var column = this.columns[columnIndex];
column.headerEditor = cellEditor;
// give access to the column from the cell editor
if (cellEditor) {
cellEditor.editablegrid = this;
cellEditor.column = column;
}
}
};
/**
* Sets the enum provider for the specified column index
* @param {Object} columnIndexOrName index or name of the column
* @param {EnumProvider} enumProvider
*/
EditableGrid.prototype.setEnumProvider = function(columnIndexOrName, enumProvider)
{
var columnIndex = this.getColumnIndex(columnIndexOrName);
if (columnIndex < 0) alert("[setEnumProvider] Invalid column: " + columnIndexOrName);
else this.columns[columnIndex].enumProvider = enumProvider;
// we must recreate the cell renderer and editor for this column
this._createCellRenderer(this.columns[columnIndex]);
this._createCellEditor(this.columns[columnIndex]);
};
/**
* Clear all cell validators for the specified column index
* @param {Object} columnIndexOrName index or name of the column
*/
EditableGrid.prototype.clearCellValidators = function(columnIndexOrName)
{
var columnIndex = this.getColumnIndex(columnIndexOrName);
if (columnIndex < 0) alert("[clearCellValidators] Invalid column: " + columnIndexOrName);
else this.columns[columnIndex].cellValidators = [];
};
/**
* Adds default cell validators for the specified column index (according to the column type)
* @param {Object} columnIndexOrName index or name of the column
*/
EditableGrid.prototype.addDefaultCellValidators = function(columnIndexOrName)
{
var columnIndex = this.getColumnIndex(columnIndexOrName);
if (columnIndex < 0) alert("[addDefaultCellValidators] Invalid column: " + columnIndexOrName);
return this._addDefaultCellValidators(this.columns[columnIndex]);
};
/**
* Adds default cell validators for the specified column
* @private
*/
EditableGrid.prototype._addDefaultCellValidators = function(column)
{
if (column.datatype == "integer" || column.datatype == "double") column.cellValidators.push(new NumberCellValidator(column.datatype));
else if (column.datatype == "email") column.cellValidators.push(new EmailCellValidator());
else if (column.datatype == "website" || column.datatype == "url") column.cellValidators.push(new WebsiteCellValidator());
else if (column.datatype == "date") column.cellValidators.push(new DateCellValidator(this));
};
/**
* Adds a cell validator for the specified column index
* @param {Object} columnIndexOrName index or name of the column
* @param {CellValidator} cellValidator
*/
EditableGrid.prototype.addCellValidator = function(columnIndexOrName, cellValidator)
{
var columnIndex = this.getColumnIndex(columnIndexOrName);
if (columnIndex < 0) alert("[addCellValidator] Invalid column: " + columnIndexOrName);
else this.columns[columnIndex].cellValidators.push(cellValidator);
};
/**
* Sets the table caption: set as null to remove
* @param columnIndexOrName
* @param caption
* @return
*/
EditableGrid.prototype.setCaption = function(caption)
{
this.caption = caption;
};
/**
* Get cell element at given row and column
*/
EditableGrid.prototype.getCell = function(rowIndex, columnIndex)
{
var row = this.getRow(rowIndex);
return row.cells[columnIndex];
};
/**
* Get cell X position relative to the first non static offset parent
* @private
*/
EditableGrid.prototype.getCellX = function(oElement)
{
var iReturnValue = 0;
while (oElement != null && this.isStatic(oElement)) try {
iReturnValue += oElement.offsetLeft;
oElement = oElement.offsetParent;
} catch(err) { oElement = null; }
return iReturnValue;
};
/**
* Get cell Y position relative to the first non static offset parent
* @private
*/
EditableGrid.prototype.getCellY = function(oElement)
{
var iReturnValue = 0;
while (oElement != null && this.isStatic(oElement)) try {
iReturnValue += oElement.offsetTop;
oElement = oElement.offsetParent;
} catch(err) { oElement = null; }
return iReturnValue;
};
/**
* Renders the grid as an HTML table in the document
* @param {String} containerid
* id of the div in which you wish to render the HTML table (this parameter is ignored if you used attachToHTMLTable)
* @param {String} className
* CSS class name to be applied to the table (this parameter is ignored if you used attachToHTMLTable)
* @param {String} tableid
* ID to give to the table (this parameter is ignored if you used attachToHTMLTable)
* @see EditableGrid#attachToHTMLTable
* @see EditableGrid#loadXML
*/
EditableGrid.prototype.renderGrid = function(containerid, className, tableid)
{
with (this) {
// if we are already attached to an existing table, just update the cell contents
if (typeof table != "undefined" && table) {
// render headers
_renderHeaders();
// render content
var rows = tBody.rows;
for (var i = 0; i < rows.length; i++) {
var rowData = [];
var cols = rows[i].cells;
for (var j = 0; j < cols.length && j < columns.length; j++)
if (columns[j].renderable) columns[j].cellRenderer._render(i, j, cols[j], getValueAt(i,j));
}
// attach handler on click or double click
if (doubleclick) table.addEventListener('dblclick', mouseClickedWrapper, false);
else table.addEventListener('click', mouseClickedWrapper, false);
}
// we must render a whole new table
else {
if (!_$(containerid)) return alert("Unable to get element [" + containerid + "]");
// create editablegrid table and add it to our container
this.table = document.createElement("table");
table.className = className || "editablegrid";
if (typeof tableid != "undefined") table.id = tableid;
while (_$(containerid).hasChildNodes()) _$(containerid).removeChild(_$(containerid).firstChild);
_$(containerid).appendChild(table);
// create header
if (caption) {
var captionElement = document.createElement("CAPTION");
captionElement.innerHTML = this.caption;
table.appendChild(captionElement);
}
this.tHead = document.createElement("THEAD");
table.appendChild(tHead);
var trHeader = tHead.insertRow(0);
var columnCount = getColumnCount();
for (var c = 0; c < columnCount; c++) {
var headerCell = document.createElement("TH");
var td = trHeader.appendChild(headerCell);
columns[c].headerRenderer._render(-1, c, td, columns[c].label);
}
// create body and rows
this.tBody = document.createElement("TBODY");
table.appendChild(tBody);
var rowCount = getRowCount();
for (i = 0; i < rowCount; i++) {
var tr = tBody.insertRow(i);
tr.id = this.name + "_" + data[i]['id'];
for (j = 0; j < columnCount; j++) {
// create cell and render its content
var td = tr.insertCell(j);
columns[j].cellRenderer._render(i, j, td, getValueAt(i,j));
}
}
// attach handler on click or double click
if (doubleclick) _$(containerid).addEventListener('dblclick', mouseClickedWrapper, false);
else _$(containerid).addEventListener('click', mouseClickedWrapper, false);
}
// resort table
sort();
// callback
tableRendered(containerid, className, tableid);
}
};
/**
* Render all column headers
* @private
*/
EditableGrid.prototype._renderHeaders = function()
{
with (this) {
var rows = tHead.rows;
for (var i = 0; i < 1 /*rows.length*/; i++) {
var rowData = [];
var cols = rows[i].cells;
var columnIndexInModel = 0;
for (var j = 0; j < cols.length && columnIndexInModel < columns.length; j++) {
columns[columnIndexInModel].headerRenderer._render(-1, columnIndexInModel, cols[j], columns[columnIndexInModel].label);
var colspan = parseInt(cols[j].getAttribute("colspan"));
columnIndexInModel += colspan > 1 ? colspan : 1;
}
}
}
};
EditableGrid.prototype.unattach = function()
{
with (this) {
if (doubleclick) table.removeEventListener('dblclick', mouseClickedWrapper, false);
else table.removeEventListener('click', mouseClickedWrapper, false);
}
};
/**
* Mouse click handler
* @param {Object} e
* @private
*/
EditableGrid.prototype.mouseClicked = function(e)
{
e = e || window.event;
with (this) {
// get row and column index from the clicked cell
var target = e.target || e.srcElement;
// go up parents to find a cell or a link under the clicked position
while (target) if (target.tagName == "A" || target.tagName == "TD" || target.tagName == "TH") break; else target = target.parentNode;
if (!target || !target.parentNode || !target.parentNode.parentNode || (target.parentNode.parentNode.tagName != "TBODY" && target.parentNode.parentNode.tagName != "THEAD") || target.hasAttribute('isEditing')) return;
// don't handle clicks on links
if (target.tagName == "A") return;
// get cell position in table
var rowIndex = target.parentNode.rowIndex - nbHeaderRows; // remove header rows
var columnIndex = target.cellIndex;
var column = columns[columnIndex];
if (column) {
// if another row has been selected: callback
if (rowIndex > -1 && rowIndex != lastSelectedRowIndex) {
rowSelected(lastSelectedRowIndex, rowIndex);
lastSelectedRowIndex = rowIndex;
}
// edit current cell value
if (!column.editable) { readonlyWarning(column); }
else {
if (rowIndex < 0) {
if (column.headerEditor && isHeaderEditable(rowIndex, columnIndex))
column.headerEditor.edit(rowIndex, columnIndex, target, column.label);
}
else if (column.cellEditor && isEditable(rowIndex, columnIndex))
column.cellEditor.edit(rowIndex, columnIndex, target, getValueAt(rowIndex, columnIndex));
}
}
}
};
/**
* Sort on a column
* @param {Object} columnIndexOrName index or name of the column
* @param {Boolean} descending
*/
EditableGrid.prototype.sort = function(columnIndexOrName, descending)
{
with (this) {
if (typeof columnIndexOrName == 'undefined') columnIndexOrName = sortedColumnName;
if (typeof descending == 'undefined') descending = sortDescending;
var columnIndex = columnIndexOrName;
if (columnIndex !== -1) {
columnIndex = this.getColumnIndex(columnIndexOrName);
if (columnIndex < 0) {
alert("[sort] Invalid column: " + columnIndexOrName);
return false;
}
}
var type = columnIndex < 0 ? "" : getColumnType(columnIndex);
var row_array = [];
var rows = tBody.rows;
for (var i = 0; i < rows.length - (ignoreLastRow ? 1 : 0); i++) row_array.push([columnIndex < 0 ? null : getValueAt(i, columnIndex), i, rows[i], data[i].originalIndex]);
row_array.sort(columnIndex < 0 ? unsort :
type == "integer" || type == "double" ? sort_numeric :
type == "boolean" ? sort_boolean :
type == "date" ? sort_date :
sort_alpha);
if (descending) row_array = row_array.reverse();
if (ignoreLastRow) row_array.push([columnIndex < 0 ? null : getValueAt(rows.length - 1, columnIndex), rows.length - 1, rows[rows.length - 1], data[rows.length - 1].originalIndex]);
var _data = data;
data = [];
for (var i = 0; i < row_array.length; i++) {
data.push(_data[row_array[i][1]]);
tBody.appendChild(row_array[i][2]);
}
delete row_array;
// callback
tableSorted();
}
};
/**
* Filter the content of the table
* @param {Element} filter Element input element used to filter
*/
EditableGrid.prototype.filter = function(str)
{
with (this) {
var words = str.toLowerCase().split(" ");
var ele;
for (var r = 1; r < table.rows.length; r++){
ele = table.rows[r].innerHTML.replace(/<[^>]+>/g,"");
var displayStyle = 'none';
for (var i = 0; i < words.length; i++) {
if (ele.toLowerCase().indexOf(words[i])>=0)
displayStyle = '';
else {
displayStyle = 'none';
break;
}
}
table.rows[r].style.display = displayStyle;
}
// callback
tableFiltered();
}
};
/*
* EditableGrid_charts.js
*
* Copyright 2010 Webismymind SPRL
*
* This file is part of EditableGrid.
*
* EditableGrid 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
* any later version.
*
* EditableGrid 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 EditableGrid. If not, see http://www.gnu.org/licenses.
*
*/
var EditableGrid_pending_charts = {};
var EditableGrid_check_lib = true;
function EditableGrid_loadChart(divId)
{
var swf = findSWF(divId);
if (swf && typeof swf.load == "function") swf.load(JSON.stringify(EditableGrid_pending_charts[divId]));
else setTimeout("EditableGrid_loadChart('"+divId+"');", 100);
}
function EditableGrid_get_chart_data(divId)
{
setTimeout("EditableGrid_loadChart('"+divId+"');", 100);
return JSON.stringify(EditableGrid_pending_charts[divId]);
}
var smartColors1 = ["#dc243c","#4040f6","#00f629","#efe100","#f93fb1","#6f8183","#111111"];
var smartColors2 = ["#FF0000","#00FF00","#0000FF","#FFD700","#FF00FF","#00FFFF","#800080"];
EditableGrid.prototype.checkChartLib = function()
{
EditableGrid_check_lib = false;
if (typeof JSON.stringify == 'undefined') { alert('This method needs the JSON javascript library'); return false; }
else if (typeof findSWF == 'undefined') { alert('This method needs the open flash chart javascript library (findSWF)'); return false; }
else if (typeof ofc_chart == 'undefined') { alert('This method needs the open flash chart javascript library (ofc_chart)'); return false; }
else if (typeof swfobject == 'undefined') { alert('This method needs the swfobject javascript library'); return false; }
else return true;
};
/**
* renderBarChart
* Render open flash bar chart for the data contained in the table model
* @param divId
* @return
*/
EditableGrid.prototype.renderBarChart = function(divId, title, labelColumnIndexOrName, legend, bgColor, alpha)
{
with (this) {
if (EditableGrid_check_lib && !checkChartLib()) return false;
if (typeof bgColor == 'undefined') bgColor = "#ffffff";
if (typeof alpha == 'undefined') alpha = 0.9;
labelColumnIndexOrName = labelColumnIndexOrName || 0;
var cLabel = getColumnIndex(labelColumnIndexOrName);
var chart = new ofc_chart();
chart.bg_colour = bgColor;
chart.set_title({text: title || '', style: "{font-size: 20px; color:#0000ff; font-family: Verdana; text-align: center;}"});
var columnCount = getColumnCount();
var rowCount = getRowCount();
var maxvalue = 0;
for (var c = 0; c < columnCount; c++) {
if (!isColumnBar(c)) continue;
var bar = new ofc_element("bar_3d");
bar.alpha = alpha;
bar.colour = smartColors1[chart.elements.length % smartColors1.length];
bar.fill = "transparent";
bar.text = getColumnLabel(c);
for (var r = 0; r < rowCount - (ignoreLastRow ? 1 : 0); r++) {
var value = getValueAt(r,c);
if (value > maxvalue) maxvalue = value;
bar.values.push(value);
}
chart.add_element(bar);
}
// round the y max value
var ymax = 10;
while (ymax < maxvalue) ymax *= 10;
var dec_step = ymax / 10;
while (ymax - dec_step > maxvalue) ymax -= dec_step;
var xLabels = [];
for (var r = 0; r < rowCount - (ignoreLastRow ? 1 : 0); r++) xLabels.push(getValueAt(r,cLabel));
chart.x_axis = {
stroke: 1,
tick_height: 10,
colour: "#E2E2E2",
"grid-colour": "#E2E2E2",
labels: { labels: xLabels },
"3d": 5
};
chart.y_axis = {
stroke: 4,
tick_length: 3,
colour: "#428BC7",
"grid-colour": "#E2E2E2",
offset: 0,
steps: ymax / 10.0,
max: ymax
};
// chart.num_decimals = 0;
chart.x_legend = {
text: legend || getColumnLabel(labelColumnIndexOrName),
style: "{font-size: 11px; color: #000033}"
};
chart.y_legend = {
text: "",
style: "{font-size: 11px; color: #000033}"
};
updateChart(divId, chart);
}
};
/**
* renderPieChart
* @param columnIndexOrName
* @param divId
* @return
*/
EditableGrid.prototype.renderPieChart = function(divId, title, valueColumnIndexOrName, labelColumnIndexOrName, startAngle, bgColor, alpha, gradientFill)
{
with (this) {
if (EditableGrid_check_lib && !checkChartLib()) return false;
if (typeof gradientFill == 'undefined') gradientFill = true;
if (typeof bgColor == 'undefined') bgColor = "#ffffff";
if (typeof alpha == 'undefined') alpha = 0.5;
var type = getColumnType(valueColumnIndexOrName);
if (type != "double" && type != "integer") return;
labelColumnIndexOrName = labelColumnIndexOrName || 0;
title = (typeof title == 'undefined' || title === null) ? getColumnLabel(valueColumnIndexOrName) : title;
var cValue = getColumnIndex(valueColumnIndexOrName);
var cLabel = getColumnIndex(labelColumnIndexOrName);
var chart = new ofc_chart();
chart.bg_colour = bgColor;
chart.set_title({text: title, style: "{font-size: 20px; color:#0000ff; font-family: Verdana; text-align: center;}"});
var rowCount = getRowCount();
var pie = new ofc_element("pie");
pie.colours = smartColors2;
pie.alpha = alpha;
pie['gradient-fill'] = gradientFill;
if (typeof startAngle != 'undefined') pie['start-angle'] = startAngle;
var total = 0;
for (var r = 0; r < rowCount - (ignoreLastRow ? 1 : 0); r++) {
var rowValue = getValueAt(r,cValue);
total += isNaN(rowValue) ? 0 : rowValue;
}
for (var r = 0; r < rowCount - (ignoreLastRow ? 1 : 0); r++) {
var value = getValueAt(r,cValue);
var label = getValueAt(r,cLabel);
if (!isNaN(value)) pie.values.push({value : value, label: label + ' (' + (100 * (value / total)).toFixed(1) + '%)'});
}
chart.add_element(pie);
if (pie.values.length > 0) updateChart(divId, chart);
return pie.values.length;
}
};
/**
* updateChart
* @param divId
* @param chart
* @return
*/
EditableGrid.prototype.updateChart = function(divId, chart)
{
if (typeof this.ofcSwf == 'undefined' || !this.ofcSwf) {
// detect openflashchart swf location
this.ofcSwf = 'open-flash-chart.swf'; // defaults to current directory
var e = document.getElementsByTagName('script');
for (var i = 0; i < e.length; i++) {
var index = e[i].src.indexOf('openflashchart');
if (index != -1) {
this.ofcSwf = e[i].src.substr(0, index + 15) + this.ofcSwf;
break;
}
};
}
with (this) {
// reload or create new swf chart
var swf = findSWF(divId);
if (swf && typeof swf.load == "function") swf.load(JSON.stringify(chart));
else {
var div = _$(divId);
EditableGrid_pending_charts[divId] = chart;
// get chart dimensions
var w = parseInt(getStyle(div, 'width'));
var h = parseInt(getStyle(div, 'height'));
w = Math.max(isNaN(w)?0:w, div.offsetWidth);
h = Math.max(isNaN(h)?0:h, div.offsetHeight);
swfobject.embedSWF(this.ofcSwf,
divId,
"" + (w || 500),
"" + (h || 200),
"9.0.0", "expressInstall.swf", { "get-data": "EditableGrid_get_chart_data", "id": divId }, null,
{ wmode: "Opaque", salign: "l", AllowScriptAccess:"always"}
);
}
chartRendered();
}
};
/**
* clearChart
* @param divId
* @return
*/
EditableGrid.prototype.clearChart = function(divId)
{
// how ?
};/*
* EditableGrid_editors.js
*
* Copyright 2010 Webismymind SPRL
*
* This file is part of EditableGrid.
*
* EditableGrid 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
* any later version.
*
* EditableGrid 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 EditableGrid. If not, see http://www.gnu.org/licenses.
*
*/
/**
* Abstract cell editor
* @constructor
* @class Base class for all cell editors
*/
function CellEditor(config) { this.init(config); }
CellEditor.prototype.init = function(config)
{
// override default properties with the ones given
if (config) for (var p in config) this[p] = config[p];
};
CellEditor.prototype.edit = function(rowIndex, columnIndex, element, value)
{
// tag element and remember all the things we need to apply/cancel edition
element.setAttribute('isEditing','true');
// call the specialized getEditor method
var editorInput = this.getEditor(element, value);
this.editorInput=editorInput;
if (!editorInput) return false;
var _this=this;
// listen to pressed keys
// - tab does not work with onkeyup (it's too late)
// - on Safari escape does not work with onkeypress
// - with onkeydown everything is fine (but don't forget to return false)
var keydownListener = function(event) {
event = event || window.event;
// ENTER or TAB: apply value
if (event.keyCode == 13 || event.keyCode == 9) {
if(blurListener) editorInput.removeEventListener('blur',blurListener,false);
editorInput.removeEventListener('keydown',keydownListener,false);
_this.applyEditing(element, _this.getEditorValue(editorInput));
event.preventDefault();
}
// ESC: cancel editing
if (event.keyCode == 27) {
if(blurListener) editorInput.removeEventListener('blur',blurListener,false);
editorInput.removeEventListener('keydown',keydownListener,false);
_this.cancelEditing(element);
event.preventDefault();
}
};
editorInput.addEventListener('keydown',keydownListener,false);
// if simultaneous edition is not allowed, we cancel edition when focus is lost
var blurListener = null;
if (!this.editablegrid.allowSimultaneousEdition) {
if (this.editablegrid.saveOnBlur) {
blurListener=function(event) {
editorInput.removeEventListener('blur',blurListener,false);
_this.applyEditing(element, _this.getEditorValue(editorInput));
}
}else{
blurListener=function(event) {
editorInput.removeEventListener('blur',blurListener,false);
_this.cancelEditing(element);
}
}
editorInput.addEventListener('blur',blurListener,false);
}
this.blurListener=blurListener;
// display the resulting editor widget
this.displayEditor(element, editorInput);
// give focus to the created editor
editorInput.focus();
};
CellEditor.prototype.getEditor = function(element, value) {
return null;
};
CellEditor.prototype.getEditorValue = function(editorInput) {
return editorInput.value;
};
CellEditor.prototype.formatValue = function(value) {
return value;
};
CellEditor.prototype.displayEditor = function(element, editorInput)
{
// use same font in input as in cell content
editorInput.style.fontFamily = this.editablegrid.getStyle(element, "fontFamily", "font-family");
editorInput.style.fontSize = this.editablegrid.getStyle(element, "fontSize", "font-size");
// static mode: add input field in the table cell
if (this.editablegrid.editmode == "static") {
while (element.hasChildNodes()) element.removeChild(element.firstChild);
element.appendChild(editorInput);
}
// absolute mode: add input field in absolute position over table cell, leaving current content
if (this.editablegrid.editmode == "absolute") {
element.appendChild(editorInput);
editorInput.style.position = "absolute";
// position editor input on the cell with the same padding as the actual cell content
var paddingLeft = parseInt(this.editablegrid.getStyle(element, "paddingLeft", "padding-left"));
var paddingTop = parseInt(this.editablegrid.getStyle(element, "paddingTop", "padding-top"));
if (isNaN(paddingLeft)) paddingLeft = 0; else paddingLeft = Math.max(0, paddingLeft - 3);
if (isNaN(paddingTop)) paddingTop = 0; else paddingTop = Math.max(0, paddingTop - 3);
var offsetScrollX = this.editablegrid.table.parentNode ? parseInt(this.editablegrid.table.parentNode.scrollLeft) : 0;
var offsetScrollY = this.editablegrid.table.parentNode ? parseInt(this.editablegrid.table.parentNode.scrollTop) : 0;
editorInput.style.left = (this.editablegrid.getCellX(element) - offsetScrollX + paddingLeft) + "px";
editorInput.style.top = (this.editablegrid.getCellY(element) - offsetScrollY + paddingTop) + "px";
// if number type: align field and its content to the right
if (this.column.datatype == 'integer' || this.column.datatype == 'double') {
var rightPadding = this.editablegrid.getCellX(element) - offsetScrollX + element.offsetWidth - (parseInt(editorInput.style.left) + editorInput.offsetWidth);
editorInput.style.left = (parseInt(editorInput.style.left) + rightPadding) + "px";
editorInput.style.textAlign = "right";
}
}
// fixed mode: don't show input field in the cell
if (this.editablegrid.editmode == "fixed") {
var editorzone = _$(this.editablegrid.editorzoneid);
while (editorzone.hasChildNodes()) editorzone.removeChild(editorzone.firstChild);
editorzone.appendChild(editorInput);
}
};
CellEditor.prototype._clearEditor = function(element)
{
// untag element
element.removeAttribute('isEditing');
// clear fixed editor zone if any
if (this.editablegrid.editmode == "fixed") {
var editorzone = _$(this.editablegrid.editorzoneid);
while (editorzone.hasChildNodes()) editorzone.removeChild(editorzone.firstChild);
}
};
CellEditor.prototype.cancelEditing = function(element)
{
with (this) {
// check that the element is still being edited (otherwise onblur will be called on textfields that have been closed when we go to another tab in Firefox)
if (element && element.hasAttribute('isEditing')) {
// render value before editon
var renderer = this == column.headerEditor ? column.headerRenderer : column.cellRenderer;
var rowIndex=element.parentNode.rowIndex - this.editableGrid.nbHeaderRows;
var columnIndex=element.cellIndex;
renderer._render(rowIndex, columnIndex, element, editablegrid.getValueAt(rowIndex, columnIndex));
_clearEditor(element);
}
}
};
CellEditor.prototype.applyEditing = function(element, newValue)
{
with (this) {
// check that the element is still being edited (otherwise onblur will be called on textfields that have been closed when we go to another tab in Firefox)
if (element && element.hasAttribute('isEditing')) {
// do nothing if the value is rejected by at least one validator
if (!column.isValid(newValue)) return false;
// format the value before applying
var formattedValue = formatValue(newValue);
var rowIndex=element.parentNode.rowIndex - this.editablegrid.nbHeaderRows;
var columnIndex=element.cellIndex;
// update model and render cell (keeping previous value)
var previousValue = editablegrid.setValueAt(rowIndex, columnIndex, formattedValue);
// if the new value is different than the previous one, let the user handle the model change
var newValue = editablegrid.getValueAt(rowIndex, columnIndex);
if (!this.editablegrid.isSame(newValue, previousValue)) {
editablegrid.modelChanged(rowIndex, columnIndex, previousValue, newValue, editablegrid.getRow(rowIndex));
}
_clearEditor(element);
}
}
};
/**
* Text cell editor
* @constructor
* @class Class to edit a cell with an HTML text input
*/
function TextCellEditor(size, maxlen, config) { this.fieldSize = size || -1; this.maxLength = maxlen || 1024; if (config) this.init(config); };
TextCellEditor.prototype = new CellEditor();
TextCellEditor.prototype.editorValue = function(value) {
return value;
};
TextCellEditor.prototype.updateStyle = function(htmlInput)
{
// change style for invalid values
if (this.column.isValid(this.getEditorValue(htmlInput))) this.editablegrid.removeClassName(htmlInput, this.editablegrid.invalidClassName);
else this.editablegrid.addClassName(htmlInput, this.editablegrid.invalidClassName);
};
TextCellEditor.prototype.getEditor = function(element, value)
{
// create and initialize text field
var htmlInput = document.createElement("input");
htmlInput.setAttribute("type", "text");
if (this.maxLength > 0) htmlInput.setAttribute("maxlength", this.maxLength);
if (this.fieldSize > 0) htmlInput.setAttribute("size", this.fieldSize);
else htmlInput.style.width = this.editablegrid.autoWidth(element) + 'px'; // auto-adapt width to cell, if no length specified
var autoHeight = this.editablegrid.autoHeight(element);
if (this.editablegrid.Browser.Gecko) autoHeight -= 2; // Firefox: input higher then given size in px!
htmlInput.style.height = autoHeight + 'px'; // auto-adapt height to cell
htmlInput.value = this.editorValue(value);
// listen to keyup to check validity and update style of input field
var _this=this;
this.keyUpListener=function(event) {
_this.updateStyle(this);
};
htmlInput.addEventListener('keyup',this.keyUpListener,false);
this.htmlInput=htmlInput;
return htmlInput;
};
TextCellEditor.prototype.displayEditor = function(element, htmlInput)
{
// call base method
CellEditor.prototype.displayEditor.call(this, element, htmlInput);
// update style of input field
this.updateStyle(htmlInput);
// select text
htmlInput.select();
};
TextCellEditor.prototype._cancelEditing=TextCellEditor.prototype.cancelEditing;
TextCellEditor.prototype._applyEditing=TextCellEditor.prototype.applyEditing;
TextCellEditor.prototype.cancelEditing = function() {
this.htmlInput.removeEventListener('keyup',this.keyUpListener,false);
this._cancelEditing.apply(this,arguments);
};
TextCellEditor.prototype.applyEditing = function() {
this.htmlInput.removeEventListener('keyup',this.keyUpListener,false);
this._applyEditing.apply(this,arguments);
};
/**
* Number cell editor
* @constructor
* @class Class to edit a numeric cell with an HTML text input
*/
function NumberCellEditor(type) { this.type = type; }
NumberCellEditor.prototype = new TextCellEditor(-1, 32);
NumberCellEditor.prototype.editorValue = function(value) {
return isNaN(value) ? "" : value;
};
NumberCellEditor.prototype.formatValue = function(value)
{
return this.type == 'integer' ? parseInt(value) : parseFloat(value);
};
/**
* Select cell editor
* @constructor
* @class Class to edit a cell with an HTML select input
*/
function SelectCellEditor() { this.minWidth = 100; this.minHeight = 22; this.adaptHeight = true; this.adaptWidth = true;}
SelectCellEditor.prototype = new CellEditor();
SelectCellEditor.prototype.getEditor = function(element, value)
{
// create select list
var htmlInput = document.createElement("select");
// auto adapt dimensions to cell, with a min width
if (this.adaptWidth) htmlInput.style.width = Math.max(this.minWidth, this.editablegrid.autoWidth(element)) + 'px';
if (this.adaptHeight) htmlInput.style.height = Math.max(this.minHeight, this.editablegrid.autoHeight(element)) + 'px';
// get column option values for this row
var optionValues = this.column.getOptionValuesForEdit(element.parentNode.rowIndex - this.editablegrid.nbHeaderRows);
// add these options, selecting the current one
var index = 0, valueFound = false;
for (var optionValue in optionValues) {
var option = document.createElement('option');
option.text = optionValues[optionValue];
option.value = optionValue;
// add does not work as expected in IE7 (cf. second arg)
try { htmlInput.add(option, null); } catch (e) { htmlInput.add(option); }
if (optionValue == value) { htmlInput.selectedIndex = index; valueFound = true; }
index++;
}
// if the current value is not in the list add it to the front
if (!valueFound) {
var option = document.createElement('option');
option.text = value ? value : "";
option.value = value ? value : "";
// add does not work as expected in IE7 (cf. second arg)
try { htmlInput.add(option, htmlInput.options[0]); } catch (e) { htmlInput.add(option); }
htmlInput.selectedIndex = 0;
}
// when a new value is selected we apply it
var _this=this;
this.changeListener=function(event) {
_this.editorInput.removeEventListener('blur',_this.blurListener,false);
_this.applyEditing(element, this.value);
};
htmlInput.addEventListener('change',this.changeListener,false);
this.htmlInput=htmlInput;
return htmlInput;
};
SelectCellEditor.prototype._cancelEditing=SelectCellEditor.prototype.cancelEditing;
SelectCellEditor.prototype._applyEditing=SelectCellEditor.prototype.applyEditing;
SelectCellEditor.prototype.cancelEditing = function() {
this.htmlInput.removeEventListener('change',this.changeListener,false);
this._cancelEditing.apply(this,arguments);
};
SelectCellEditor.prototype.applyEditing = function() {
this.htmlInput.removeEventListener('change',this.changeListener,false);
this._applyEditing.apply(this,arguments);
};
/*
* EditableGrid_renderers.js
*
* Copyright 2010 Webismymind SPRL
*
* This file is part of EditableGrid.
*
* EditableGrid 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
* any later version.
*
* EditableGrid 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 EditableGrid. If not, see http://www.gnu.org/licenses.
*
*/
/**
* Abstract cell renderer
* @constructor
* @class Base class for all cell renderers
* @param {Object} config
*/
function CellRenderer(config) { this.init(config); }
CellRenderer.prototype.init = function(config)
{
// override default properties with the ones given
for (var p in config) this[p] = config[p];
};
CellRenderer.prototype._render = function(rowIndex, columnIndex, element, value)
{
// remove existing content
while (element.hasChildNodes()) element.removeChild(element.firstChild);
// always apply the number style to numerical cells and column headers
if (this.column.isNumerical()) EditableGrid.prototype.addClassName(element, "number");
// call the specialized render method
return this.render(element, typeof value == 'string' ? htmlspecialchars(value, 'ENT_NOQUOTES').replace(/\s\s/g, ' ') : value);
};
CellRenderer.prototype.render = function(element, value)
{
element.innerHTML = value ? value : "";
};
/**
* Enum cell renderer
* @constructor
* @class Class to render a cell with enum values
*/
function EnumCellRenderer(config) { this.init(config); }
EnumCellRenderer.prototype = new CellRenderer();
EnumCellRenderer.prototype.render = function(element, value)
{
var optionValues = this.column.getOptionValuesForRender(element.parentNode.rowIndex - this.editablegrid.nbHeaderRows);
element.innerHTML = (typeof value != 'undefined' ? (value in optionValues ? optionValues[value] : value) : "");
};
/**
* Number cell renderer
* @constructor
* @class Class to render a cell with numerical values
*/
function NumberCellRenderer(config) { this.init(config); }
NumberCellRenderer.prototype = new CellRenderer();
NumberCellRenderer.prototype.render = function(element, value)
{
var column = this.column || {}; // in case somebody calls new NumberCellRenderer().render(..)
var isNAN = typeof value == 'number' && isNaN(value);
var displayValue = isNAN ? (column.nansymbol || "") : value;
if (typeof displayValue == 'number') {
if (column.precision !== null) displayValue = displayValue.toFixed(column.precision);
if (column.unit !== null) displayValue += ' ' + column.unit;
}
element.innerHTML = displayValue;
element.style.fontWeight = isNAN ? "normal" : "";
};
/**
* Checkbox cell renderer
* @constructor
* @class Class to render a cell with an HTML checkbox
*/
function CheckboxCellRenderer(config) { this.init(config); }
CheckboxCellRenderer.prototype = new CellRenderer();
CheckboxCellRenderer.prototype._render = function(rowIndex, columnIndex, element, value)
{
// if a checkbox already exists keep it, otherwise clear current content
if (element.firstChild && (typeof element.firstChild.getAttribute != "function" || element.firstChild.getAttribute("type") != "checkbox"))
while (element.hasChildNodes()) element.removeChild(element.firstChild);
// call the specialized render method
return this.render(element, value);
};
CheckboxCellRenderer.prototype.render = function(element, value)
{
// convert value to boolean just in case
value = (value && value != 0 && value != "false") ? true : false;
// if check box already created, just update its state
if (element.firstChild) { element.firstChild.checked = value; return; }
// create and initialize checkbox
var htmlInput = document.createElement("input");
htmlInput.setAttribute("type", "checkbox");
// this renderer is a little special because it allows direct edition
var cellEditor = new CellEditor();
cellEditor.editablegrid = this.editablegrid;
cellEditor.column = this.column;
var _this=this;
var clickListener=function(event) {
// in case it has changed due to sorting or remove
element.setAttribute('isEditing','true');
cellEditor.applyEditing(element, htmlInput.checked ? true : false);
};
htmlInput.addEventListener('click',clickListener,false);
element.appendChild(htmlInput);
htmlInput.checked = value;
htmlInput.disabled = (!this.column.editable || !this.editablegrid.isEditable(element.parentNode.rowIndex - this.editablegrid.nbHeaderRows, element.cellIndex));
element.className = "boolean";
};
/**
* Email cell renderer
* @constructor
* @class Class to render a cell with emails
*/
function EmailCellRenderer(config) { this.init(config); }
EmailCellRenderer.prototype = new CellRenderer();
EmailCellRenderer.prototype.render = function(element, value)
{
element.innerHTML = value ? "<a href='mailto:" + value + "'>" + value + "</a>" : "";
};
/**
* Website cell renderer
* @constructor
* @class Class to render a cell with websites
*/
function WebsiteCellRenderer(config) { this.init(config); }
WebsiteCellRenderer.prototype = new CellRenderer();
WebsiteCellRenderer.prototype.render = function(element, value)
{
element.innerHTML = value ? "<a href='" + (value.indexOf("//") == -1 ? "http://" + value : value) + "'>" + value + "</a>" : "";
};
/**
* Date cell renderer
* @constructor
* @class Class to render a cell containing a date
*/
function DateCellRenderer(config) { this.init(config); }
DateCellRenderer.prototype = new CellRenderer;
DateCellRenderer.prototype.render = function(cell, value)
{
var date = this.editablegrid.checkDate(value);
if (typeof date == "object") cell.innerHTML = date.formattedDate;
else cell.innerHTML = value;
};
/**
* Sort header renderer
* @constructor
* @class Class to add sorting functionalities to headers
*/
function SortHeaderRenderer(columnName, cellRenderer) { this.columnName = columnName; this.cellRenderer = cellRenderer; };
SortHeaderRenderer.prototype = new CellRenderer();
SortHeaderRenderer.prototype.render = function(cell, value)
{
if (!value) { if (this.cellRenderer) this.cellRenderer.render(cell, value); }
else {
// create a link that will sort (alternatively ascending/descending)
var link = document.createElement("a");
cell.appendChild(link);
link.style.cursor = "pointer";
link.innerHTML = value;
var _this=this;
var clickListener=function() {
with (_this.editablegrid) {
var cols = tHead.rows[0].cells;
var clearPrevious = -1;
if (sortedColumnName != _this.columnName) {
clearPrevious = sortedColumnName;
sortedColumnName = _this.columnName;
sortDescending = false;
}
else {
if (!sortDescending) sortDescending = true;
else {
clearPrevious = sortedColumnName;
sortedColumnName = -1;
sortDescending = false;
}
}
// render header for previous sort column
var j = getColumnIndex(clearPrevious);
if (j >= 0) columns[j].headerRenderer._render(-1, j, cols[j], columns[j].label);
sort(sortedColumnName, sortDescending);
// render header for new sort column
var j = getColumnIndex(sortedColumnName);
if (j >= 0) columns[j].headerRenderer._render(-1, j, cols[j], columns[j].label);
}
};
link.addEventListener('click',clickListener,false);
// add an arrow to indicate if sort is ascending or descending
if (this.editablegrid.sortedColumnName == this.columnName) {
cell.appendChild(document.createTextNode("\u00a0"));
cell.appendChild(this.editablegrid.sortDescending ? this.editablegrid.sortDownImage: this.editablegrid.sortUpImage);
}
// call user renderer if any
if (this.cellRenderer) this.cellRenderer.render(cell, value);
}
};
/*
* EditableGrid_utils.js
*
* Copyright 2010 Webismymind SPRL
*
* This file is part of EditableGrid.
*
* EditableGrid 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
* any later version.
*
* EditableGrid 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 EditableGrid. If not, see http://www.gnu.org/licenses.
*
*/
EditableGrid.prototype.unsort = function(a,b)
{
aa = isNaN(a[3]) ? 0 : parseFloat(a[3]);
bb = isNaN(b[3]) ? 0 : parseFloat(b[3]);
return aa-bb;
};
EditableGrid.prototype.sort_numeric = function(a,b)
{
aa = isNaN(a[0]) ? 0 : parseFloat(a[0]);
bb = isNaN(b[0]) ? 0 : parseFloat(b[0]);
return aa-bb;
};
EditableGrid.prototype.sort_boolean = function(a,b)
{
aa = !a[0] || a[0] == "false" ? 0 : 1;
bb = !b[0] || b[0] == "false" ? 0 : 1;
return aa-bb;
};
EditableGrid.prototype.sort_alpha = function(a,b)
{
if (a[0]==b[0]) return 0;
if (a[0]<b[0]) return -1;
return 1;
};
EditableGrid.prototype.sort_date = function(a,b)
{
date = EditableGrid.prototype.checkDate(a[0]);
aa = typeof date == "object" ? date.sortDate : 0;
date = EditableGrid.prototype.checkDate(b[0]);
bb = typeof date == "object" ? date.sortDate : 0;
return aa-bb;
};
/**
* Returns computed style property for element
* @private
*/
EditableGrid.prototype.getStyle = function(element, stylePropCamelStyle, stylePropCSSStyle)
{
stylePropCSSStyle = stylePropCSSStyle || stylePropCamelStyle;
if (element.currentStyle) return element.currentStyle[stylePropCamelStyle];
else if (window.getComputedStyle) return document.defaultView.getComputedStyle(element,null).getPropertyValue(stylePropCSSStyle);
return element.style[stylePropCamelStyle];
};
/**
* Returns true if the element has a static positioning
* @private
*/
EditableGrid.prototype.isStatic = function (element)
{
var position = this.getStyle(element, 'position');
return (!position || position == "static");
};
/**
* Returns auto width for editor
* @private
*/
EditableGrid.prototype.autoWidth = function (element)
{
var paddingLeft = parseInt(this.getStyle(element, "paddingLeft", "padding-left"));
var paddingRight = parseInt(this.getStyle(element, "paddingRight", "padding-right"));
var borderLeft = parseInt(this.getStyle(element, "borderLeftWidth", "border-left-width"));
var borderRight = parseInt(this.getStyle(element, "borderRightWidth", "border-right-width"));
paddingLeft = isNaN(paddingLeft) ? 0 : paddingLeft;
paddingRight = isNaN(paddingRight) ? 0 : paddingRight;
borderLeft = isNaN(borderLeft) ? 0 : borderLeft;
borderRight = isNaN(borderRight) ? 0 : borderRight;
if (this.Browser.Gecko) paddingLeft += 3; // Firefox: input larger then given size in px!
return element.offsetWidth - paddingLeft - paddingRight - borderLeft - borderRight;
};
/**
* Returns auto height for editor
* @private
*/
EditableGrid.prototype.autoHeight = function (element)
{
var paddingTop = parseInt(this.getStyle(element, "paddingTop", "padding-top"));
var paddingBottom = parseInt(this.getStyle(element, "paddingBottom", "padding-bottom"));
var borderTop = parseInt(this.getStyle(element, "borderTopWidth", "border-top-width"));
var borderBottom = parseInt(this.getStyle(element, "borderBottomWidth", "border-bottom-width"));
paddingTop = isNaN(paddingTop) ? 0 : paddingTop;
paddingBottom = isNaN(paddingBottom) ? 0 : paddingBottom;
borderTop = isNaN(borderTop) ? 0 : borderTop;
borderBottom = isNaN(borderBottom) ? 0 : borderBottom;
return element.offsetHeight - paddingTop - paddingBottom - borderTop - borderBottom;
};
/**
* Detects the directory when the js sources can be found
* @private
*/
EditableGrid.prototype.detectDir = function()
{
var base = location.href;
var e = document.getElementsByTagName('base');
for (var i=0; i<e.length; i++) if(e[i].href) base = e[i].href;
var e = document.getElementsByTagName('script');
for (var i=0; i<e.length; i++) {
if (e[i].src && /(^|\/)editablegrid.*\.js([?#].*)?$/i.test(e[i].src)) {
var src = new URI(e[i].src);
var srcAbs = src.toAbsolute(base);
srcAbs.path = srcAbs.path.replace(/[^\/]+$/, ''); // remove filename
delete srcAbs.query;
delete srcAbs.fragment;
return srcAbs.toString();
}
}
return false;
};
/**
* Detect is 2 values are exactly the same (type and value). Numeric NaN are considered the same.
* @param v1
* @param v2
* @return boolean
*/
EditableGrid.prototype.isSame = function(v1, v2)
{
if (v1 === v2) return true;
if (typeof v1 == 'number' && isNaN(v1) && typeof v2 == 'number' && isNaN(v2)) return true;
return false;
};
/**
* class name manipulation
* @private
*/
EditableGrid.prototype.strip = function(str) { return str.replace(/^\s+/, '').replace(/\s+$/, ''); };
EditableGrid.prototype.hasClassName = function(element, className) { return (element.className.length > 0 && (element.className == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(element.className))); };
EditableGrid.prototype.addClassName = function(element, className) { if (!this.hasClassName(element, className)) element.className += (element.className ? ' ' : '') + className; };
EditableGrid.prototype.removeClassName = function(element, className) { element.className = this.strip(element.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ')); };
/**
* Useful string methods
* @private
*/
String.prototype.trim = function() { return (this.replace(/^[\s\xA0]+/, "").replace(/[\s\xA0]+$/, "")); };
String.prototype.startsWith = function(str) { return (this.match("^"+str)==str); };
String.prototype.endsWith = function(str) { return (this.match(str+"$")==str); };
// Accepted formats: (for EU just switch month and day)
//
// mm-dd-yyyy
// mm/dd/yyyy
// mm.dd.yyyy
// mm dd yyyy
// mmm dd yyyy
// mmddyyyy
//
// m-d-yyyy
// m/d/yyyy
// m.d.yyyy,
// m d yyyy
// mmm d yyyy
//
// // m-d-yy
// // m/d/yy
// // m.d.yy
// // m d yy,
// // mmm d yy (yy is 20yy)
/**
* Checks validity of a date string
* @private
*/
EditableGrid.prototype.checkDate = function(strDate, strDatestyle)
{
strDatestyle = strDatestyle || "EU";
var strDate;
var strDateArray;
var strDay;
var strMonth;
var strYear;
var intday;
var intMonth;
var intYear;
var booFound = false;
var strSeparatorArray = new Array("-"," ","/",".");
var intElementNr;
var err = 0;
var strMonthArray = new Array(12);
strMonthArray = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
if (!strDate || strDate.length < 1) return 0;
for (intElementNr = 0; intElementNr < strSeparatorArray.length; intElementNr++) {
if (strDate.indexOf(strSeparatorArray[intElementNr]) != -1) {
strDateArray = strDate.split(strSeparatorArray[intElementNr]);
if (strDateArray.length != 3) return 1;
else {
strDay = strDateArray[0];
strMonth = strDateArray[1];
strYear = strDateArray[2];
}
booFound = true;
}
}
if (booFound == false) {
if (strDate.length <= 5) return 1;
strDay = strDate.substr(0, 2);
strMonth = strDate.substr(2, 2);
strYear = strDate.substr(4);
}
// if (strYear.length == 2) strYear = '20' + strYear;
// US style
if (strDatestyle == "US") {
strTemp = strDay;
strDay = strMonth;
strMonth = strTemp;
}
// get and check day
intday = parseInt(strDay, 10);
if (isNaN(intday)) return 2;
// get and check month
intMonth = parseInt(strMonth, 10);
if (isNaN(intMonth)) {
for (i = 0;i<12;i++) {
if (strMonth.toUpperCase() == strMonthArray[i].toUpperCase()) {
intMonth = i+1;
strMonth = strMonthArray[i];
i = 12;
}
}
if (isNaN(intMonth)) return 3;
}
if (intMonth>12 || intMonth<1) return 5;
// get and check year
intYear = parseInt(strYear, 10);
if (isNaN(intYear)) return 4;
if (intYear < 1900 || intYear > 2100) return 11;
// check day in month
if ((intMonth == 1 || intMonth == 3 || intMonth == 5 || intMonth == 7 || intMonth == 8 || intMonth == 10 || intMonth == 12) && (intday > 31 || intday < 1)) return 6;
if ((intMonth == 4 || intMonth == 6 || intMonth == 9 || intMonth == 11) && (intday > 30 || intday < 1)) return 7;
if (intMonth == 2) {
if (intday < 1) return 8;
if (LeapYear(intYear) == true) { if (intday > 29) return 9; }
else if (intday > 28) return 10;
}
// return formatted date
return {
formattedDate: (strDatestyle == "US" ? strMonthArray[intMonth-1] + " " + intday+" " + strYear : intday + " " + strMonthArray[intMonth-1]/*.toLowerCase()*/ + " " + strYear),
sortDate: Date.parse(intMonth + "/" + intday + "/" + intYear),
dbDate: intYear + "-" + intMonth + "-" + intday
};
};
function LeapYear(intYear)
{
if (intYear % 100 == 0) { if (intYear % 400 == 0) return true; }
else if ((intYear % 4) == 0) return true;
return false;
}
// See RFC3986
URI = function(uri)
{
this.scheme = null;
this.authority = null;
this.path = '';
this.query = null;
this.fragment = null;
this.parse = function(uri) {
var m = uri.match(/^(([A-Za-z][0-9A-Za-z+.-]*)(:))?((\/\/)([^\/?#]*))?([^?#]*)((\?)([^#]*))?((#)(.*))?/);
this.scheme = m[3] ? m[2] : null;
this.authority = m[5] ? m[6] : null;
this.path = m[7];
this.query = m[9] ? m[10] : null;
this.fragment = m[12] ? m[13] : null;
return this;
};
this.toString = function() {
var result = '';
if(this.scheme != null) result = result + this.scheme + ':';
if(this.authority != null) result = result + '//' + this.authority;
if(this.path != null) result = result + this.path;
if(this.query != null) result = result + '?' + this.query;
if(this.fragment != null) result = result + '#' + this.fragment;
return result;
};
this.toAbsolute = function(base) {
var base = new URI(base);
var r = this;
var t = new URI;
if(base.scheme == null) return false;
if(r.scheme != null && r.scheme.toLowerCase() == base.scheme.toLowerCase()) {
r.scheme = null;
}
if(r.scheme != null) {
t.scheme = r.scheme;
t.authority = r.authority;
t.path = removeDotSegments(r.path);
t.query = r.query;
} else {
if(r.authority != null) {
t.authority = r.authority;
t.path = removeDotSegments(r.path);
t.query = r.query;
} else {
if(r.path == '') {
t.path = base.path;
if(r.query != null) {
t.query = r.query;
} else {
t.query = base.query;
}
} else {
if(r.path.substr(0,1) == '/') {
t.path = removeDotSegments(r.path);
} else {
if(base.authority != null && base.path == '') {
t.path = '/'+r.path;
} else {
t.path = base.path.replace(/[^\/]+$/,'')+r.path;
}
t.path = removeDotSegments(t.path);
}
t.query = r.query;
}
t.authority = base.authority;
}
t.scheme = base.scheme;
}
t.fragment = r.fragment;
return t;
};
function removeDotSegments(path) {
var out = '';
while(path) {
if(path.substr(0,3)=='../' || path.substr(0,2)=='./') {
path = path.replace(/^\.+/,'').substr(1);
} else if(path.substr(0,3)=='/./' || path=='/.') {
path = '/'+path.substr(3);
} else if(path.substr(0,4)=='/../' || path=='/..') {
path = '/'+path.substr(4);
out = out.replace(/\/?[^\/]*$/, '');
} else if(path=='.' || path=='..') {
path = '';
} else {
var rm = path.match(/^\/?[^\/]*/)[0];
path = path.substr(rm.length);
out = out + rm;
}
}
return out;
}
if(uri) {
this.parse(uri);
}
};
function get_html_translation_table (table, quote_style) {
// http://kevin.vanzonneveld.net
// + original by: Philip Peterson
// + revised by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// + bugfixed by: noname
// + bugfixed by: Alex
// + bugfixed by: Marco
// + bugfixed by: madipta
// + improved by: KELAN
// + improved by: Brett Zamir (http://brett-zamir.me)
// + bugfixed by: Brett Zamir (http://brett-zamir.me)
// + input by: Frank Forte
// + bugfixed by: T.Wild
// + input by: Ratheous
// % note: It has been decided that we're not going to add global
// % note: dependencies to php.js, meaning the constants are not
// % note: real constants, but strings instead. Integers are also supported if someone
// % note: chooses to create the constants themselves.
// * example 1: get_html_translation_table('HTML_SPECIALCHARS');
// * returns 1: {'"': '"', '&': '&', '<': '<', '>': '>'}
var entities = {}, hash_map = {}, decimal = 0, symbol = '';
var constMappingTable = {}, constMappingQuoteStyle = {};
var useTable = {}, useQuoteStyle = {};
// Translate arguments
constMappingTable[0] = 'HTML_SPECIALCHARS';
constMappingTable[1] = 'HTML_ENTITIES';
constMappingQuoteStyle[0] = 'ENT_NOQUOTES';
constMappingQuoteStyle[2] = 'ENT_COMPAT';
constMappingQuoteStyle[3] = 'ENT_QUOTES';
useTable = !isNaN(table) ? constMappingTable[table] : table ? table.toUpperCase() : 'HTML_SPECIALCHARS';
useQuoteStyle = !isNaN(quote_style) ? constMappingQuoteStyle[quote_style] : quote_style ? quote_style.toUpperCase() : 'ENT_COMPAT';
if (useTable !== 'HTML_SPECIALCHARS' && useTable !== 'HTML_ENTITIES') {
throw new Error("Table: "+useTable+' not supported');
// return false;
}
entities['38'] = '&';
if (useTable === 'HTML_ENTITIES') {
entities['160'] = ' ';
entities['161'] = '¡';
entities['162'] = '¢';
entities['163'] = '£';
entities['164'] = '¤';
entities['165'] = '¥';
entities['166'] = '¦';
entities['167'] = '§';
entities['168'] = '¨';
entities['169'] = '©';
entities['170'] = 'ª';
entities['171'] = '«';
entities['172'] = '¬';
entities['173'] = '­';
entities['174'] = '®';
entities['175'] = '¯';
entities['176'] = '°';
entities['177'] = '±';
entities['178'] = '²';
entities['179'] = '³';
entities['180'] = '´';
entities['181'] = 'µ';
entities['182'] = '¶';
entities['183'] = '·';
entities['184'] = '¸';
entities['185'] = '¹';
entities['186'] = 'º';
entities['187'] = '»';
entities['188'] = '¼';
entities['189'] = '½';
entities['190'] = '¾';
entities['191'] = '¿';
entities['192'] = 'À';
entities['193'] = 'Á';
entities['194'] = 'Â';
entities['195'] = 'Ã';
entities['196'] = 'Ä';
entities['197'] = 'Å';
entities['198'] = 'Æ';
entities['199'] = 'Ç';
entities['200'] = 'È';
entities['201'] = 'É';
entities['202'] = 'Ê';
entities['203'] = 'Ë';
entities['204'] = 'Ì';
entities['205'] = 'Í';
entities['206'] = 'Î';
entities['207'] = 'Ï';
entities['208'] = 'Ð';
entities['209'] = 'Ñ';
entities['210'] = 'Ò';
entities['211'] = 'Ó';
entities['212'] = 'Ô';
entities['213'] = 'Õ';
entities['214'] = 'Ö';
entities['215'] = '×';
entities['216'] = 'Ø';
entities['217'] = 'Ù';
entities['218'] = 'Ú';
entities['219'] = 'Û';
entities['220'] = 'Ü';
entities['221'] = 'Ý';
entities['222'] = 'Þ';
entities['223'] = 'ß';
entities['224'] = 'à';
entities['225'] = 'á';
entities['226'] = 'â';
entities['227'] = 'ã';
entities['228'] = 'ä';
entities['229'] = 'å';
entities['230'] = 'æ';
entities['231'] = 'ç';
entities['232'] = 'è';
entities['233'] = 'é';
entities['234'] = 'ê';
entities['235'] = 'ë';
entities['236'] = 'ì';
entities['237'] = 'í';
entities['238'] = 'î';
entities['239'] = 'ï';
entities['240'] = 'ð';
entities['241'] = 'ñ';
entities['242'] = 'ò';
entities['243'] = 'ó';
entities['244'] = 'ô';
entities['245'] = 'õ';
entities['246'] = 'ö';
entities['247'] = '÷';
entities['248'] = 'ø';
entities['249'] = 'ù';
entities['250'] = 'ú';
entities['251'] = 'û';
entities['252'] = 'ü';
entities['253'] = 'ý';
entities['254'] = 'þ';
entities['255'] = 'ÿ';
}
if (useQuoteStyle !== 'ENT_NOQUOTES') {
entities['34'] = '"';
}
if (useQuoteStyle === 'ENT_QUOTES') {
entities['39'] = ''';
}
entities['60'] = '<';
entities['62'] = '>';
// ascii decimals to real symbols
for (decimal in entities) {
symbol = String.fromCharCode(decimal);
hash_map[symbol] = entities[decimal];
}
return hash_map;
}
function htmlentities(string, quote_style)
{
var hash_map = {}, symbol = '', tmp_str = '';
tmp_str = string.toString();
if (false === (hash_map = get_html_translation_table('HTML_ENTITIES', quote_style))) return false;
hash_map["'"] = ''';
for (symbol in hash_map) tmp_str = tmp_str.split(symbol).join(hash_map[symbol]);
return tmp_str;
}
function htmlspecialchars(string, quote_style)
{
var hash_map = {}, symbol = '', tmp_str = '';
tmp_str = string.toString();
if (false === (hash_map = get_html_translation_table('HTML_SPECIALCHARS', quote_style))) return false;
for (symbol in hash_map) tmp_str = tmp_str.split(symbol).join(hash_map[symbol]);
return tmp_str;
}/*
* EditableGrid_validators.js
*
* Copyright 2010 Webismymind SPRL
*
* This file is part of EditableGrid.
*
* EditableGrid 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
* any later version.
*
* EditableGrid 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 EditableGrid. If not, see http://www.gnu.org/licenses.
*
*/
/**
* Abstract cell validator
* @constructor
* @class Base class for all cell validators
*/
function CellValidator(config)
{
// default properties
var props = { isValid: null };
// override default properties with the ones given
for (var p in props) if (typeof config != 'undefined' && typeof config[p] != 'undefined') this[p] = config[p];
}
CellValidator.prototype.isValid = function(value)
{
return true;
};
/**
* Number cell validator
* @constructor
* @class Class to validate a numeric cell
*/
function NumberCellValidator(type) { this.type = type; }
NumberCellValidator.prototype = new CellValidator;
NumberCellValidator.prototype.isValid = function(value)
{
// check that it is a valid number
if (isNaN(value)) return false;
// for integers check that it's not a float
if (this.type == "integer" && value != "" && parseInt(value) != parseFloat(value)) return false;
// the integer or double is valid
return true;
};
/**
* Email cell validator
* @constructor
* @class Class to validate a cell containing an email
*/
function EmailCellValidator() {}
EmailCellValidator.prototype = new CellValidator;
EmailCellValidator.prototype.isValid = function(value) { return value == "" || /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/.test(value); };
/**
* Website cell validator
* @constructor
* @class Class to validate a cell containing a website
*/
function WebsiteCellValidator() {}
WebsiteCellValidator.prototype = new CellValidator;
WebsiteCellValidator.prototype.isValid = function(value) { return value == "" || (value.indexOf(".") > 0 && value.indexOf(".") < (value.length - 2)); };
/**
* Date cell validator
* @constructor
* @augments CellValidator
* @class Class to validate a cell containing a date
*/
function DateCellValidator(grid) { this.grid = grid; }
DateCellValidator.prototype = new CellValidator;
DateCellValidator.prototype.isValid = function(value)
{
return value == "" || typeof this.grid.checkDate(value) == "object";
};
/**
pref
*/
/**
base classes
*/
function draggable(dragButton,dragObj){
let obj=this;
this.dragBegin=function(e){
let dragObj=obj.dragObj;
if(isNaN(parseInt(dragObj.style.left))) dragObj.style.left='0px';
if(isNaN(parseInt(dragObj.style.top))) dragObj.style.top='0px';
let x=parseInt(dragObj.style.left);
let y=parseInt(dragObj.style.top);
obj.mouseX=e.clientX;
obj.mouseY=e.clientY;
window.addEventListener('mousemove',obj.drag,false);
window.addEventListener('mouseup',obj.dragEnd,false);
return false;
};
this.drag=function(e){
let dragObj=obj.dragObj;
let x=parseInt(dragObj.style.left);
let y=parseInt(dragObj.style.top);
dragObj.style.left=x+(e.clientX-obj.mouseX)+'px';
dragObj.style.top=y+(e.clientY-obj.mouseY)+'px';
obj.mouseX=e.clientX;
obj.mouseY=e.clientY;
return false;
};
this.dragEnd=function(){
window.removeEventListener('mousemove',obj.drag,false);
window.removeEventListener('mouseup',obj.dragEnd,false);
};
this.finalize=function(){
this.dragButton.removeEventListener('mousedown',this.dragBegin,false);
};
dragButton.addEventListener('mousedown',this.dragBegin,false);
//fields
this.dragButton=dragButton;
this.dragObj=dragObj;
this.mouseX=0;
this.mouseY=0;
}
/**
attaches draggable and exit functions to a pref modal dialog
*/
let prefModalObjs=[];
function prefModal(prefObj,titleBar,titleExitButton,exitButton,exitFunc){
if(exitFunc==undefined) exitFunc=new Function();
function _exitFunc(){
exitFunc();
dragObj.finalize();
titleExitButton.removeEventListener('click',_exitFunc,false);
exitButton.removeEventListener('click',_exitFunc,false);
titleExitButton.removeEventListener('mousedown',titleNoDrag,false);
if(prefModalObjs.length==1){
window.removeEventListener('keypress',escapeKeyPress,false);
}
prefObj.style.display='none';
}
function titleNoDrag(e){
//prevent dragging exit button
e.preventDefault();
//prevent dragging prefs using exit button
e.stopPropagation();
}
function escapeKeyPress(e){
if(e.keyCode==27){
//close current pref dialog only
prefModalObjs[prefModalObjs.length-1].exit();
prefModalObjs.pop();
}
e.stopPropagation();
}
titleExitButton.addEventListener('click',_exitFunc,false);
exitButton.addEventListener('click',_exitFunc,false);
titleExitButton.addEventListener('mousedown',titleNoDrag,false);
if(prefModalObjs.length==0){
window.addEventListener('keypress',escapeKeyPress,false);
}
//add dragging to titleBar
let dragObj=new draggable(titleBar,prefObj);
//display modal dialog
prefObj.style.display='';
//fields
this.exit=_exitFunc;
//update objects
prefModalObjs.push(this);
}
/**
preferences
*/
let pref={};
/**
constants
*/
pref.c={
guiNodeId: 'googleSearchFilterPlus',
tGuiNodeId: 'tGoogleSearchFilterPlus',
guiSettings: 'guiSettings',
}
/**
globals
*/
pref.guiObj=null;
pref.tGuiObj=null;
/**
pref settings:
everything except for individual filters
*/
pref.settings={
load: function(){
let res=GM_getValue(pref.c.guiSettings);
if(res==undefined){
pref.s={}
pref.settings.save();
}else{
pref.s=JSON.parse(res);
}
},
save: function(){
GM_setValue(pref.c.guiSettings,JSON.stringify(pref.s));
},
}
/**
optional function, use when html needs to be loaded dynamically
only called once at init()
*/
pref.loadHTML=function(){
}
/**
main pref gui
*/
pref.gui={
modalObj: null,
init: function(){
pref.gui.modalObj=new prefModal(pref.guiObj,_$('titleBar'),_$('exitButton'),_$('cancelButton'),pref.gui.close);
pref.gui.load();
_$('saveButton').addEventListener('click',pref.gui.save,false);
},
load: function(){
let guiObj=pref.guiObj;
let mouseX=pref.s.mouseX;
let mouseY=pref.s.mouseY;
if(!mouseX || !mouseY){
//default x to middle of page, y to upper half of page
mouseX=(window.innerWidth-guiObj.clientWidth)/2;
mouseY=(window.innerHeight-guiObj.clientHeight)/3;
}
guiObj.style.left=mouseX+'px';
guiObj.style.top=mouseY+'px';
},
save: function(){
//grid gui settings are flushed when pref is closed
pref.grid.grid2filters();
gfpFilter.save();
},
close: function(){
let guiObj=pref.guiObj;
pref.s.mouseX=guiObj.offsetLeft;
pref.s.mouseY=guiObj.offsetTop;
pref.grid.save();
pref.settings.save();
if(!pref.chg.isEmpty()){
pref.grid.finalize();
}
},
}
/**
import/export modal dialog
*/
pref.tGui={
modalObj: null,
init: function(titleStr,saveFunc){
if(pref.tGui.modalObj) return null;
_$('tTitle').innerHTML=titleStr;
let modalObj=new prefModal(pref.tGuiObj,_$('tTitleBar'),_$('tExitButton'),_$('tCancelButton'),function(){
_$('tText').value='';
_$('tTitle').innerHTML='';
pref.tGui.modalObj=null;
saveButton.removeEventListener('click',_saveFunc,false);
});
let saveButton=_$('tOkButton');
function _saveFunc(){
saveFunc();
modalObj.exit();
}
saveButton.addEventListener('click',_saveFunc,false);
pref.tGui.modalObj=modalObj;
pref.tGui.load();
return modalObj;
},
load: function(){
let tGuiObj=pref.tGuiObj;
//default x, y to middle of page
mouseX=(window.innerWidth-tGuiObj.clientWidth)/2;
mouseY=(window.innerHeight-tGuiObj.clientHeight)/2;
tGuiObj.style.left=mouseX+'px';
tGuiObj.style.top=mouseY+'px';
},
}
/**
tracks and merges filter changes
*/
pref.chg={
//current filters (added/changed filters)
filters: {},
//removed filters
removed: {},
//currFilters==knownFilters, used so knownFilters only need to be copied once
// when this is true, filters is the changes need to be made to knownFilters to make it current
// when this is false, filters is a copy of the current filters
currIsDelta: true,
isRemoved: function(keys,len){
return pref.chg.getRemoved(keys,len)==true;
},
getRemoved: function(keys,len){
/**
returns removed (does not have to be true/false)
*/
if(!len) len=keys.length;
let removed=pref.chg.removed;
for(let i=0;i<len;i++){
removed=removed[keys[i]];
if(!removed) return null;
}
return removed;
},
setRemoved: function(keys,len){
if(!len) len=keys.length;
len--;
let removed=pref.chg.removed;
for(let i=0;i<len;i++){
if(keys[i] in removed){
removed=removed[keys[i]];
}else{
for(;i<len;i++){
let tmp={};
removed[keys[i]]=tmp;
removed=tmp;
}
break;
}
}
removed[keys[len]]=true;
},
removeFilters: function(filters,removed){
for(let key in removed){
if(removed[key]==true){
delete filters[key];
}else if(removed[key]!=undefined){
pref.chg.removeFilters(filters[key].subfilters,removed[key]);
}
}
},
removeFilter: function(text){
/**
doesn't assume text exists
*/
let keys=gfpFilter.getKeys(text);
if(pref.chg.currIsDelta){
let ret=gfpFilter.getSubfilter(keys,Filter.knownFilters);
if(ret){
pref.chg.setRemoved(keys);
return ret;
}else{
return gfpFilter.popSubfilter(keys,pref.chg.filters);
}
}else{
return gfpFilter.popSubfilter(keys,pref.chg.filters);
}
},
getFilter: function(text){
/**
doesn't assume text exists
*/
let keys=gfpFilter.getKeys(text);
if(pref.chg.currIsDelta){
let ret=gfpFilter.getSubfilter(keys,Filter.knownFilters);
//check if filter is removed
if(ret){
if(!pref.chg.isRemoved(keys)){
return ret;
}
}
return gfpFilter.getSubfilter(keys,pref.chg.filters);
}else{
return gfpFilter.getSubfilter(keys,pref.chg.filters);
}
},
hasFilter: function(text){
let keys=gfpFilter.getKeys(text);
let level=gfpFilter.isDuplicate(keys,Filter.knownFilters);
if(level){
if(!pref.chg.isRemoved(keys,level)){
return true;
}
}
return gfpFilter.isDuplicate(keys,pref.chg.filters)>0;
},
addFilter: function(filter){
/**
args:
filter: filter text object
*/
return gfpFilter.fromObjectCompiled(filter,pref.chg.filters);
},
mergeFiltersFlush: function(filters,filtersM,removed){
pref.chg.removeFilters(filters,removed);
pref.chg._mergeFiltersFlush(filters,filtersM);
},
_mergeFiltersFlush: function(filters,filtersM){
/**
merges filters and flushes to filters
uses pointers from filtersM
*/
//faster going through filtersM as this is likely to be smaller
for(let text in filtersM){
if(text in filters){
let filter=filters[text];
let filterM=filtersM[text];
if('subfilters' in filterM){
//can't check if subfilters is empty without a loop
if('subfilters' in filter){
//go deeper
pref.chg._mergeFiltersFlush(filter.subfilters,filterM.subfilters);
}else{
//copy to filter
filter.subfilters=filterM.subfilters;
}
}else{
//filters are not necessarily equal if final subfilter (hitcount, etc.)
filters[text]=filterM;
}
}else{
filters[text]=filtersM[text];
}
}
},
mergeFiltersFlushM: function(filters,filtersM,removed){
/**
merges filters
uses pointers from filtersM
*/
//javascript doesn't care if the iterator is changed when iterating, so can flush to filtersM directly
for(let text in filters){
let rem=removed[text];
if(rem!=true){
if(text in filtersM){
let filter=filters[text];
let filterM=filtersM[text];
if('subfilters' in filter){
if('subfilters' in filterM){
//will not happen when 'a$$b$$c' --> 'a$$b'
pref.chg.mergeFiltersFlushM(filter.subfilters,filterM.subfilters,rem||{});
}
}
}else if(rem==undefined){
filtersM[text]=filters[text];
}else{
//clone filter, since it can't be modified
let filter=gfpFilter.clone(filters[text]);
filter.subfilters={};
filtersM[text]=filter;
pref.chg.mergeFiltersFlushM(filter.subfilters,{},rem);
}
}
}
return filtersM;
},
mergeFilters: function(filters){
/**
merges filters with currFilters
*/
pref.chg.filters=pref.chg.mergeFiltersFlushM(pref.chg.filters,filters,{});
return pref.chg.filters;
},
flush: function(flush2known){
/**
flushes changes (removes filters delta)
args:
flush2known: if true, flushes to knownFilters; if false, makes filters a copy of currFilters
*/
if(flush2known){
if(pref.chg.currIsDelta){
pref.chg.mergeFiltersFlush(Filter.knownFilters,pref.chg.filters,pref.chg.removed);
}else{
Filter.knownFilters=pref.chg.filters;
}
pref.chg.clear();
return Filter.knownFilters;
}else{
if(pref.chg.currIsDelta){
pref.chg.mergeFiltersFlushM(Filter.knownFilters,pref.chg.filters,pref.chg.removed);
pref.chg.currIsDelta=false;
}
return pref.chg.filters;
}
},
isEmpty: function(){
for(let key in pref.chg.filters) return false;
if(pref.chg.currIsDelta){
for(let key in pref.chg.removed) return false;
}
return true;
},
clear: function(){
pref.chg.filters={};
pref.chg.removed={};
pref.chg.currIsDelta=true;
},
init: function(filters){
/**
initializes filters to filters
assumes this is not a pointer to knownFilters
*/
pref.chg.filters=filters;
pref.chg.removed={};
pref.chg.currIsDelta=false;
},
}
/**
filterGrid
grid gui, add, import, export, load, save
*/
pref.grid={
//current row index, used to specify the next inserted row id
currRowId: 0,
//editableGrid obj
editableGrid: null,
//html of header before render
headerHTML: null,
//last td column style node
lastColumnStyle: [],
cellWidthSub: null,
lastCellWidth: 0,
filterGrid: null,
load: function(){
/**
loads grid gui information: column sort
this and filters2grid should be independant, since the save button saves the filter settings,
and doesn't need to save the gui information
*/
let editableGrid=pref.grid.editableGrid;
let sortedColumnName=pref.s.sortedColumnName;
let sortDescending=pref.s.sortDescending;
if(sortedColumnName==undefined || sortDescending==undefined){
sortedColumnName='filter';
sortDescending=false;
}
editableGrid.sortedColumnName=sortedColumnName;
editableGrid.sortDescending=sortDescending;
editableGrid.sort();
editableGrid.columns[0].headerRenderer._render(-1,0,editableGrid.tHead.rows[0].cells[0],editableGrid.columns[0].label);
},
save: function(){
/**
saves grid gui information
*/
let editableGrid=pref.grid.editableGrid;
pref.s.sortedColumnName=editableGrid.sortedColumnName;
pref.s.sortDescending=editableGrid.sortDescending;
},
grid2filters: function(flush){
/**
pref.chg.flush
defaults flush to true
*/
if(flush==undefined) flush=true;
return pref.chg.flush(flush);
},
filters2grid: function(filters){
/**
if grid exists: swap gui filters with filters
else: render new grid from filters
*/
let editableGrid=pref.grid.editableGrid;
if(editableGrid){
//save grid gui info
pref.grid.save();
}
let filterGridBody=pref.grid.filterGrid.childNodes[3];
let currRowId=0;
let rowsHTML='';
gfpFilter.iterate(filters,function(filter,filterParts){
rowsHTML+=pref.grid.renderRowHTML(filter,filterParts,currRowId);
currRowId++;
});
filterGridBody.innerHTML=rowsHTML;
//reset changes
if(gfpFilter.isPtr(filters)){
pref.chg.clear();
}else{
pref.chg.init(filters);
}
pref.grid.currRowId=currRowId;
//render grid
pref.grid.gridInit();
},
addFilter: function(){
let editableGrid=pref.grid.editableGrid;
let currRowId=pref.grid.currRowId;
pref.grid.addRow(currRowId.toString(),{
'filter': '',
'slow': false,
'enabled': true,
'hits': 0,
'action': currRowId.toString(),
});
pref.grid.currRowId++;
let rowIndex=editableGrid.getRowIndex(currRowId);
let row=editableGrid.getRow(rowIndex);
//hook isSame to make sure modelChanged always fires
let o_isSame=editableGrid.isSame;
editableGrid.isSame=function() false;
//override modelChanged to get added filter
let o_modelChanged=editableGrid.modelChanged;
editableGrid.modelChanged=function(rowIndex,columnIndex,oldValue,newValue,row){
//create new filter object
let filterText=newValue;
if(pref.chg.hasFilter(filterText)){
alert('Filter already exists');
pref.grid.removeRow(currRowId);
//restore isSame
editableGrid.isSame=o_isSame;
//restore modelChanged
editableGrid.modelChanged=o_modelChanged;
return;
}
let filterTextObj={'text':filterText};
let filter=pref.chg.addFilter(filterTextObj);
//if filter is empty
if(filter==null){
pref.grid.removeRow(currRowId);
//restore isSame
editableGrid.isSame=o_isSame;
//restore modelChanged
editableGrid.modelChanged=o_modelChanged;
return;
}
//render new filter row
pref.grid.renderRow(filter,pref.chg.filters,row);
//re-sort table
pref.grid.editableGrid.sort();
row.scrollIntoView(false);
//restore isSame
editableGrid.isSame=o_isSame;
//restore modelChanged
editableGrid.modelChanged=o_modelChanged;
};
//edit filter
row.scrollIntoView(false);
editableGrid.columns[0].cellEditor.edit(rowIndex,0,row.childNodes[0],'');
},
importFilters: function(){
pref.tGui.init('Import Filters',function(){
let newFilters;
try{
newFilters=gfpFilter.parseCompiled(_$('tText').value);
}catch(e){
alert('Invalid import format');
return;
}
let mergeFilters=confirm('Merge with current filters?');
if(mergeFilters){
//merge changes
pref.grid.grid2filters(false);
newFilters=pref.chg.mergeFilters(newFilters);
}else{
pref.chg.init(newFilters);
}
//render new grid
pref.grid.filters2grid(newFilters);
});
},
exportFilters: function(){
let res=pref.tGui.init('Export Filters',new Function());
if(res){
_$('tText').value=gfpFilter.stringify(pref.grid.grid2filters(false));
}
},
updateLastColumnWidth: function(){
/**
adjusts right-most column (header, body) to fill up all space
*/
let filterGrid=pref.grid.filterGrid;
let cellWidthSub;
if(!pref.grid.cellWidthSub){
//initialize width for the first time
let lastHeaderCell=filterGrid.tHead.firstElementChild.lastElementChild;
//cell border+padding
let cStyles=window.getComputedStyle(lastHeaderCell,null);
let cellWidthBP=lastHeaderCell.offsetWidth-lastHeaderCell.clientWidth+parseInt(cStyles.paddingLeft)+parseInt(cStyles.paddingRight);
cellWidthSub=cellWidthBP+lastHeaderCell.offsetLeft;
pref.grid.cellWidthSub=cellWidthSub;
//add last header style
let style=document.createElement('style');
style.innerHTML='.gfp .filterGrid>thead>tr>th:last-child {width: '+(filterGrid.tHead.clientWidth-cellWidthSub)+'px;}';
pref.grid.lastColumnStyle.push(style);
document.body.appendChild(style);
//add last cell style
style=document.createElement('style');
pref.grid.lastColumnStyle.push(style);
document.body.appendChild(style);
}else{
cellWidthSub=pref.grid.cellWidthSub;
}
let lastCellWidth=filterGrid.childNodes[3].clientWidth-cellWidthSub;
if(lastCellWidth!=pref.grid.lastCellWidth){
//update last cell style node
pref.grid.lastColumnStyle[1].innerHTML='.gfp .filterGrid>tbody>tr>td:last-child {width: '+lastCellWidth+'px;}';
pref.grid.lastCellWidth=lastCellWidth;
}
},
getFilterClass: function(filter){
if(filter instanceof WhitelistFilter){
return 'whitelistFilter';
}else if(filter instanceof InvalidFilter){
return 'invalidFilter';
}else if(filter instanceof CommentFilter){
return 'commentFilter';
}
},
renderRow: function(filter,filters,row){
/**
render an existing row
processes filter to determine if filter is slow
args:
filters: filters which contain filter
row: the row element if exists, renders the element; if not, returns the html
*/
let editableGrid=pref.grid.editableGrid;
let text=(filter.fullText||filter.text);
let keys=gfpFilter.getKeys(text);
let isDisabled=(filter.disabled==undefined?false:filter.disabled);
if(isDisabled){
row.classList.add('disabledFilter');
}else{
row.classList.remove('disabledFilter');
}
let className=pref.grid.getFilterClass(filter);
let rowIndex=row.rowIndex-editableGrid.nbHeaderRows;
row.childNodes[0].setAttribute('class',className);
editableGrid.setValueAt(rowIndex,0,text);
editableGrid.setValueAt(rowIndex,1,gfpFilter.isSlowFilterKeys(keys,filters));
editableGrid.setValueAt(rowIndex,2,!isDisabled);
editableGrid.setValueAt(rowIndex,3,filter.hitCount||0);
},
renderRowHTML: function(filter,filterParts,rowId){
/**
render raw html of row
args:
rowId: row's id
*/
let rowHTML='';
let text=(filter.fullText||filter.text);
let isDisabled=(filter.disabled==undefined?false:filter.disabled);
if(isDisabled){
rowHTML+='<tr id="'+rowId.toString()+'" class="disabledFilter">';
}else{
rowHTML+='<tr id="'+rowId.toString()+'">';
}
let className=pref.grid.getFilterClass(filter);
if(className){
rowHTML+='<td class="'+className+'">'+text+'</td>';
}else{
rowHTML+='<td>'+text+'</td>';
}
rowHTML+='<td>'+gfpFilter.isSlowFilter(filter,filterParts).toString()+'</td>';
rowHTML+='<td>'+(!isDisabled).toString()+'</td>';
rowHTML+='<td>'+(filter.hitCount||0).toString()+'</td>';
rowHTML+='<td>'+rowId.toString()+'</td>';
rowHTML+='</tr>';
return rowHTML;
},
addRow: function(rowId,data){
pref.grid.editableGrid.addRow(rowId,data);
pref.grid.updateLastColumnWidth();
},
removeRow: function(rowId){
pref.grid.editableGrid.removeRow(rowId);
pref.grid.updateLastColumnWidth();
},
removeRowFilter: function(row,rowId){
let filterText=row.childNodes[0].textContent;
pref.chg.removeFilter(filterText);
pref.grid.removeRow(rowId);
},
gridInit: function(){
let filterGrid=pref.grid.filterGrid;
//check if header is already rendered
if(pref.grid.headerHTML){
filterGrid.tHead.innerHTML=pref.grid.headerHTML;
}else{
pref.grid.headerHTML=filterGrid.tHead.innerHTML;
}
if(pref.grid.editableGrid){
//unattach previous grid from table
pref.grid.editableGrid.unattach();
}
let editableGrid=new EditableGrid('filterGrid',{
enableSort: true,
editmode: 'static', //static, fixed, absolute
editorzoneid: 'edition', //used only if editmode is set to 'fixed'
});
editableGrid.sortUpImage.src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAHCAQAAABwkq/rAAAAAXNSR0IArs4c6QAAAAJiS0dEAP+Hj8y/AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH2QoQCjY2qH0pQwAAAF9JREFUCNdj/M+ADpgg1BaJNa8XyyEJbZD4fJ9B5OONLjmo0BqJz3eYONgY2Dg/3ihSZGBgYmD4dJeZm53hBwMnAw/nl3UMDCwMDB+5njN8Y/jO8IPhJ8MvdQYGRkwbAY5kGttJJR0zAAAAAElFTkSuQmCC';
editableGrid.sortDownImage.src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAHCAQAAABwkq/rAAAAAXNSR0IArs4c6QAAAAJiS0dEAP+Hj8y/AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH2QoQCjYJHhsEfgAAAGhJREFUCNddyLEJg0AYhuFXchxGHcA+TXo3cRNrtxDJPpnBKhAIuSqtIP932lwakZCnfLLEPwf3tLESEYapKxzITkWFJ2djfoCD5aKnL898eEstZAkYa3uVeZCuQ9gL+lpTbG4Bjvr1BdwZJPIY3iMMAAAAAElFTkSuQmCC';
editableGrid.attachToHTMLTable(filterGrid,[
new Column({name: 'filter', datatype: 'string'}),
new Column({name: 'slow', datatype: 'boolean'}),
new Column({name: 'enabled', datatype: 'boolean'}),
new Column({name: 'hits', datatype: 'integer'}),
new Column({name: 'action', datatype: 'html', editable: false})
]);
with(editableGrid){
setCellRenderer('slow',new CellRenderer({
render: function(cell,value){
cell.innerHTML=value?'<div class="gfpImage_7"/>':'';
}
}));
addCellValidator('hits',new CellValidator({
isValid: function(value){
return parseInt(value)>=0;
}
}));
//render delete filter column
setCellRenderer('action',new CellRenderer({render: function(cell,value){
let node=document.createElement('a');
node.setAttribute('style','cursor:pointer;');
node.innerHTML='<div alt="Delete filter" border="0" class="gfpImage_3" title="Delete filter"/>';
node.addEventListener('click',function(){
if(confirm('Are you sure you want to delete this filter?')){
pref.grid.removeRowFilter(cell.parentNode,value);
}
},false);
cell.appendChild(node);
}}));
modelChanged=function(rowIndex,columnIndex,oldValue,newValue,row){
//save filter changes
//save old filter to copy it's properties
let o_filter;
let filterText;
if(columnIndex==0){
let o_filterText=oldValue;
filterText=newValue;
//check if filterText already exists
if(pref.chg.hasFilter(filterText) && !gfpFilter.isDuplicateText(o_filterText,filterText)){
alert('Filter already exists');
this.setValueAt(rowIndex,columnIndex,oldValue);
return;
}
//remove old filter since filter name has changed
o_filter=pref.chg.removeFilter(o_filterText);
}else{
filterText=row.childNodes[0].textContent;
o_filter=pref.chg.getFilter(filterText);
}
//create new filter object
let filterTextObj={};
filterTextObj.text=(o_filter.fullText||o_filter.text);
filterTextObj.disabled=(o_filter.disabled||false).toString();
filterTextObj.hitCount=(o_filter.hitCount||0).toString();
switch(columnIndex){
case 0: filterTextObj.text=newValue; break;
case 2: filterTextObj.disabled=(!newValue).toString(); break;
case 3: filterTextObj.hitCount=newValue.toString(); break;
}
let filter=pref.chg.addFilter(filterTextObj);
//if filter is empty
if(filter==null){
pref.grid.removeRow(editableGrid.getRowId(rowIndex));
return;
}
//render new filter row
pref.grid.renderRow(filter,pref.chg.filters,row);
if(columnIndex==0){
//re-sort table
pref.grid.editableGrid.sort();
row.scrollIntoView(false);
}
};
renderGrid();
}
pref.grid.editableGrid=editableGrid;
//render first then update, since this might add a previously not present scrollbar
pref.grid.updateLastColumnWidth();
pref.grid.load();
},
init: function(){
pref.grid.filterGrid=_$('filterGrid');
pref.chg.init({});
pref.grid.filters2grid(Filter.knownFilters);
_$('addFilterButton').addEventListener('click',pref.grid.addFilter,false);
_$('importFiltersButton').addEventListener('click',pref.grid.importFilters,false);
_$('exportFiltersButton').addEventListener('click',pref.grid.exportFilters,false);
},
finalize: function(){
//restore default values
with(pref.grid){
currRowId=0;
editableGrid=null;
cellWidthSub=null;
lastCellWidth=0;
}
let lastColumnStyle=pref.grid.lastColumnStyle;
for(let i=0;i<lastColumnStyle.length;i++){
let style=lastColumnStyle[i];
style.parentNode.removeChild(style);
}
pref.grid.lastColumnStyle=[];
pref.chg.clear();
_$('addFilterButton').removeEventListener('click',pref.grid.addFilter,false);
_$('importFiltersButton').removeEventListener('click',pref.grid.importFilters,false);
_$('exportFiltersButton').removeEventListener('click',pref.grid.exportFilters,false);
},
}
/**
init
*/
pref.init=function(){
pref.loadHTML();
pref.guiObj=_$(pref.c.guiNodeId);
pref.tGuiObj=_$(pref.c.tGuiNodeId);
gfpFilter.compileAll(Filter.knownFilters);
}
pref.renderAll=function(){
if(pref.grid.editableGrid){
pref.grid.finalize();
}else{
pref.init();
}
pref.settings.load();
pref.gui.init();
pref.grid.init();
}
pref.show=function(){
if(pref.grid.editableGrid){
if(pref.guiObj.style.display!='none') return;
}else{
pref.init();
}
pref.settings.load();
pref.gui.init();
if(!pref.grid.editableGrid){
pref.grid.init();
}
}
init();
pref.loadHTML=function(){let node=document.createElement("div");node.innerHTML=<><![CDATA[<style>.gfpImage_0{width:9px;height:7px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAHCAQAAABwkq/rAAAAAXNSR0IArs4c6QAAAAJiS0dEAP+Hj8y/AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH2QoQCjYJHhsEfgAAAGhJREFUCNddyLEJg0AYhuFXchxGHcA+TXo3cRNrtxDJPpnBKhAIuSqtIP932lwakZCnfLLEPwf3tLESEYapKxzITkWFJ2djfoCD5aKnL898eEstZAkYa3uVeZCuQ9gL+lpTbG4Bjvr1BdwZJPIY3iMMAAAAAElFTkSuQmCC);background-size:100%;}
.gfpImage_1{width:9px;height:7px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAHCAQAAABwkq/rAAAAAXNSR0IArs4c6QAAAAJiS0dEAP+Hj8y/AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH2QoQCjY2qH0pQwAAAF9JREFUCNdj/M+ADpgg1BaJNa8XyyEJbZD4fJ9B5OONLjmo0BqJz3eYONgY2Dg/3ihSZGBgYmD4dJeZm53hBwMnAw/nl3UMDCwMDB+5njN8Y/jO8IPhJ8MvdQYGRkwbAY5kGttJJR0zAAAAAElFTkSuQmCC);background-size:100%;}
.gfpImage_2{width:0px;height:0px;background-image:url(data:image/gif;base64,R0lGODlhAQAWAMQAAMjKysXHx9/h4c/R0ePl5cbIyNPV1c3Pz+fp6d7h4e7w8Ovt7cvNzdfZ2eHj49vd3fDy8gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAAAAAAALAAAAAABABYAAAUSICQqC0IIT3MEBcAMRvMkjhMCADs=);background-size:100%;}
.gfpImage_3{width:16px;height:16px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJdSURBVDjLpZP7S1NhGMf9W7YfogSJboSEUVCY8zJ31trcps6zTI9bLGJpjp1hmkGNxVz4Q6ildtXKXzJNbJRaRmrXoeWx8tJOTWptnrNryre5YCYuI3rh+8vL+/m8PA/PkwIg5X+y5mJWrxfOUBXm91QZM6UluUmthntHqplxUml2lciF6wrmdHriI0Wx3xw2hAediLwZRWRkCPzdDswaSvGqkGCfq8VEUsEyPF1O8Qu3O7A09RbRvjuIttsRbT6HHzebsDjcB4/JgFFlNv9MnkmsEszodIIY7Oaut2OJcSF68Qx8dgv8tmqEL1gQaaARtp5A+N4NzB0lMXxon/uxbI8gIYjB9HytGYuusfiPIQcN71kjgnW6VeFOkgh3XcHLvAwMSDPohOADdYQJdF1FtLMZPmslvhZJk2ahkgRvq4HHUoWHRDqTEDDl2mDkfheiDgt8pw340/EocuClCuFvboQzb0cwIZgki4KhzlaE6w0InipbVzBfqoK/qRH94i0rgokSFeO11iBkp8EdV8cfJo0yD75aE2ZNRvSJ0lZKcBXLaUYmQrCzDT6tDN5SyRqYlWeDLZAg0H4JQ+Jt6M3atNLE10VSwQsN4Z6r0CBwqzXesHmV+BeoyAUri8EyMfi2FowXS5dhd7doo2DVII0V5BAjigP89GEVAtda8b2ehodU4rNaAW+dGfzlFkyo89GTlcrHYCLpKD+V7yeeHNzLjkp24Uu1Ed6G8/F8qjqGRzlbl2H2dzjpMg1KdwsHxOlmJ7GTeZC/nesXbeZ6c9OYnuxUc3fmBuFft/Ff8xMd0s65SXIb/gAAAABJRU5ErkJggg==);background-size:100%;}
.gfpImage_4{width:32px;height:32px;background-image:url(data:image/png;base64,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);background-size:100%;}
.gfpImage_5{width:32px;height:32px;background-image:url(data:image/png;base64,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);background-size:100%;}
.gfpImage_6{width:32px;height:32px;background-image:url(data:image/png;base64,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);background-size:100%;}
.gfpImage_7{width:26px;height:16px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAQCAMAAAA/D5+aAAABWVBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACheQDX19c8LgCIZgCVlZUaGhoiIiJuUwBYWFh/XwByVgBmTQAuLi6WcQAqKiodHR3Jyck0JwBtUgCLaAAgGAAyJgBUQACCgoK4uLhzVgAaEwB3WgBYQgAqHwAeHh4jIyNkZGR1WABAMAC/v78pHwCEYwBMOQBOOgB5WwCFZACWlpaSbgCDYgATExNHR0cFBQUwJQCGZQCIZwBRPQAMCQCXl5c7LAAWFhaFhYUhISFhYWE2NjZDMwAHBweSkpJ6XABLS0sVFRUKCABISEhrUQBJNwAdFgBOOwCYmJgUFBQA9R6HAAAAKHRSTlMAgpRMfYSBS36Zut28HF0Ua2ppmyWcOpGKPGXCYaHhibauhm11LYAjiwhFZAAAAPxJREFUeF5lzVVTAzEYRuEUaFF3dzlf6oq7u7u76/8XkixDLzgXO/Puk0yUVy6uAjfyMR9yPCmEVDIZgxJnVJmzpU6qWRZXiGZLRagWnJSRkN928Rmqo7y43hEpke2Vzztre+40PiqsVCLyyMvbB0Fj3+Z5H/EBagzlGeLCvTQsslirYFVrAoZ6kOkZsWUQScPgkDbU4N3KEBMZGZWx8Qkmp7Tpi0ZDTYjMEpwjJPMshC0sRWhVpjbWRNY3NrfknR1tC9NuoWM/og8OjxLHJ6dnnGsXncoGl1d2wfWN9rpF/dbFfVxne0gbydYdf4LnKECUV/W/Xn+gz9//Bz+04TZYU20HQAAAAABJRU5ErkJggg==);background-size:100%;}</style>
<style>/*
* EditableGrid.css
*
* Copyright 2010 Webismymind SPRL
*
* This file is part of EditableGrid.
*
* EditableGrid 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
* any later version.
*
* EditableGrid 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 EditableGrid. If not, see http://www.gnu.org/licenses.
*
*/
td.number {
text-align: right;
font-weight: bold;
padding-right: 5px;
white-space: nowrap;
}
.boolean {
text-align: center;
}
</style>
<style type="text/css">
.gfp {
color: #333333;
border: 1px solid #CCCCCC;
background-color: #FFFFFF;
font-family: "Lucida Grande","Bitstream Vera Sans",Verdana,Arial,sans-serif;
font-size: 12px;
padding: 0;
width: 550px;
position: absolute;
z-index: 9999;
}
.gfp a {
text-decoration: none;
color: #999999;
font-size: 11px;
}
.gfp .prefRow {
margin-top: 3px;
overflow: hidden;
padding: 8px 12px;
}
.gfp .caption {
margin: 0;
padding: 2px 5px 3px 5px;
font-size: 11px;
font-weight: bold;
background: #CCCCCC url('data:image/gif;base64,R0lGODlhAQAWAMQAAMjKysXHx9/h4c/R0ePl5cbIyNPV1c3Pz+fp6d7h4e7w8Ovt7cvNzdfZ2eHj49vd3fDy8gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAAAAAAALAAAAAABABYAAAUSICQqC0IIT3MEBcAMRvMkjhMCADs=') top left repeat-x;
color: #666666;
border-bottom: 0;
}
.gfp .iconButton {
border-style: none;
height: 16px;
width: 16px;
}
/* buttons */
.gfp input[type="button"] {
padding: 0px 10px 0px 10px;
color: #999999;
background-color: White;
font-weight: bold;
border: solid 1px #CCCCCC;
text-align: center;
font-size:12px;
}
.gfp input[type="button"]:hover {
padding: 1px 11px 1px 11px;
color: #333333;
border-color: #666666;
}
/* filterGrid */
.gfp .filterGrid {
border-collapse: collapse;
border: 1px solid #D5D5D5;
width: 100%;
}
.gfp .whitelistFilter{
color: #008800;
}
.gfp .invalidFilter{
color: #C00000;
}
.gfp .commentFilter{
color: #808080;
}
.gfp .disabledFilter{
background-color: #E0E0E0;
}
.gfp .filterGrid input.invalid {
background: red;
color: #FDFDFD;
}
/**/
.gfp .filterGrid>thead {
font-size: 11px;
display: block;
}
.gfp .filterGrid th {
background: #E5E5E5;
border: 1px solid #D5D5D5;
color: #555;
text-align: left;
}
.gfp .filterGrid a:hover {
text-decoration: underline;
}
.gfp .filterGrid>tbody {
display: block;
height: 150px;
overflow-x: hidden;
overflow-y: auto;
color: black;
}
.gfp .filterGrid>tbody tr:hover {
background-color: #888888;
color: white;
}
.gfp .filterGrid td {
border-left: 1px solid #E0E0E0;
border-right: 1px solid #E0E0E0;
font-size: 11px;
}
/**/
.gfp .filterGrid th:nth-child(1), .gfp .filterGrid td:nth-child(1) {
width: 320px;
max-width: 320px;
overflow-x: hidden;
white-space: nowrap;
}
.gfp .filterGrid th:nth-child(2), .gfp .filterGrid td:nth-child(2) {
width: 30px;
}
.gfp .filterGrid th:nth-child(3), .gfp .filterGrid td:nth-child(3) {
width: 70px;
}
.gfp .filterGrid th:nth-child(4), .gfp .filterGrid td:nth-child(4) {
padding-right: 5px;
width: 40px;
max-width: 40px;
overflow-x: hidden;
}
#edition {
padding-top: 5px;
width: 600px;
border: 0px;
}
</style>
<!-- preferences !-->
<div id="googleSearchFilterPlus" class="gfp" style="display:none;">
<h2 id="titleBar" class="caption" style="-moz-user-select:none;">
<span style="cursor:default">Google Search Filter Plus</span>
<a id="exitButton" href="javascript:void(0);" style="float:right;">[X]</a>
</h2>
<div class="prefRow">
<a class="iconButton gfpImage_4" href="javascript:void(0);" style="display:inline-block" id="addFilterButton" title="Add filter"></a>
<a class="iconButton gfpImage_6" href="javascript:void(0);" style="display:inline-block" id="importFiltersButton" title="Import filters"></a>
<a class="iconButton gfpImage_5" href="javascript:void(0);" style="display:inline-block" id="exportFiltersButton" title="Export filters"></a>
<table id="filterGrid" class="filterGrid">
<thead>
<tr>
<th>Filter rule</th>
<th>!</th>
<th>Enabled</th>
<th>Hits</th>
<th></th>
</tr>
</thead>
<tbody>
<tr id="1">
<td>test1</td>
<td>true</td>
<td>true</td>
<td>1</td>
<td>1</td>
</tr>
<tr id="2">
<td>test2</td>
<td>false</td>
<td>true</td>
<td>2</td>
<td>2</td>
</tr>
<tr id="3">
<td>test3</td>
<td>false</td>
<td>true</td>
<td>3</td>
<td>3</td>
</tr>
<tr id="4">
<td>test4</td>
<td>false</td>
<td>true</td>
<td>4</td>
<td>4</td>
</tr>
<tr id="5">
<td>test5</td>
<td>false</td>
<td>true</td>
<td>5</td>
<td>5</td>
</tr>
</tbody>
</table>
<div id="edition"></div>
</div>
<div class="prefRow">
<input id="cancelButton" type="button" value="Cancel" style="float:right;"></input>
<input id="saveButton" type="button" value="Save" style="float:right; margin-right:8px;"></input>
</div>
</div>
<!-- import/export textarea !-->
<div id="tGoogleSearchFilterPlus" class="gfp" style="display:none;">
<h2 id="tTitleBar" class="caption" style="-moz-user-select:none;">
<span id="tTitle" style="cursor:default"> </span>
<a id="tExitButton" href="javascript:void(0);" style="float:right;">[X]</a>
</h2>
<div class="prefRow">
<textarea id="tText" wrap="off" spellcheck="false" style="height:200px; width:100%;"></textarea>
</div>
<div class="prefRow">
<input id="tCancelButton" type="button" value="Cancel" style="float:right;"></input>
<input id="tOkButton" type="button" value="OK" style="float:right; margin-right:8px;"></input>
</div>
</div>
]]></>.toString();document.body.appendChild(node);};