Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Unified Diff: options.js

Issue 6088024630755328: issue 1526 - Implement new options page design for Chrome/Opera/Safari (Closed)
Patch Set: Created Jan. 28, 2015, 3:59 p.m.
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: options.js
===================================================================
new file mode 100644
--- /dev/null
+++ b/options.js
@@ -0,0 +1,708 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2015 Eyeo GmbH
+ *
+ * Adblock Plus is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * Adblock Plus 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 Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+"use strict";
+
+(function()
+{
+ var optionSubscriptions = {};
+ var acceptableAdsUrl = null;
+
+ function onDOMLoaded()
+ {
+ initTabs();
+ updateVersionNumber();
+ updateShareLink();
+ populateLists();
+
+ E("find-language").setAttribute("placeholder", ext.i18n.getMessage("options_modal_language_find"));
+ setLinks("block-element-explanation", "#");
+
+ E("add-blocking-list").addEventListener("click", Modal.open, false);
+ E("add-website-language").addEventListener("click", Modal.open, false);
+ E("modal-close").addEventListener("click", Modal.close, false);
+ E("whitelisting-add-icon").addEventListener("click", whitelistDomainBtnClick, false);
+ E("whitelisting-add-btn").addEventListener("click", whitelistDomainBtnClick, false);
+ E("whitelisting-enter-icon").addEventListener("click", whitelistDomainBtnClick, false);
+ E("whitelisting-textbox").addEventListener("keypress", function(e) {
+ if (e.keyCode == 13)
+ whitelistDomainBtnClick();
+ }, false);
+ E("whitelisting-cancel-btn").addEventListener("click", function(){
+ E("whitelisting-textbox").value = "";
+ }, false);
+ E("allow-whitelist-cb").addEventListener("click", toggleAcceptableAds, false);
+ E("import-blockingList-btn").addEventListener("click", importListBtnCLick, false);
+ E("edit-ownBlockingList-btn").addEventListener("click", editOwnRulsBtnClick, false);
+ E("find-language").addEventListener("keyup", searchLanguage, false);
+ }
+
+ function initTabs()
+ {
+ var showContent = function(tab)
+ {
+ var tab = tab.querySelector(".tabs li.active");
+ if (tab.dataset.show)
+ E(tab.dataset.show).classList.add("active");
Thomas Greiner 2015/01/30 13:55:52 Using CSS classes instead of modifying the style a
saroyanm 2015/02/13 10:57:11 Yes, good point.
+ };
+ var optionList = document.querySelectorAll('.tabs li[data-show]');
+ for (var i = 0; i < optionList.length; ++i)
+ {
+ optionList[i].addEventListener("click", function(ev)
+ {
+ var tab = this.parentNode.querySelector(".active");
+ tab.classList.remove("active");
+ this.classList.add("active");
+ E(tab.dataset.show).classList.remove("active");
+ showContent(this.parentNode);
+ }, false);
+ }
+ showContent(E("main-navigation-tabs"));
+ showContent(E("blocking-list-tabs"));
+ }
+
+ var Modal =
+ {
+ open: function (content)
+ {
+ var modal = E("modal");
+ var content = E(this && this.dataset ? this.dataset.show : content);
+ content.classList.add("active");
+ document.body.classList.add("modal-active");
Thomas Greiner 2015/01/30 13:55:52 You could get rid of this entire function by setti
saroyanm 2015/02/13 10:57:11 Done.
+ if (content.dataset.title)
+ E("modal-title").innerHTML = ext.i18n.getMessage(content.dataset.title);
+ modal.style.marginTop = -(modal.clientHeight/2)+"px";
+ },
+ close: function ()
+ {
+ var contents = E("modal-content").childNodes;
+ for (var i = 0; i < contents.length; ++i)
+ {
+ if (contents[i].style)
+ contents[i].classList.remove("active");
+ }
+ document.body.classList.remove("modal-active");
+ }
+ }
+
+ function populateLists()
+ {
+ ext.backgroundPage.sendMessage({
+ type: "subscriptions.get",
+ special: true
+ }, function(subscriptions)
+ {
+ for (var i = 0; i < subscriptions.length; i++)
+ {
+ ext.backgroundPage.sendMessage({
+ type: "filters.get",
+ subscriptionUrl: subscriptions[i].url
+ }, function(filters)
+ {
+ var whitelistArray = [];
+ for (var i = 0; i < filters.length; i++)
+ {
+ var match = filters[i].text.match(/^@@\|\|([^\/:]+)\^\$document$/);
+ if (match[1])
+ {
+ whitelistArray.push(match[1]);
+ }
+ else
+ {
+ // TODO: add `filters[i].text` to list of custom filters
+ }
+ }
+
+ if (whitelistArray.length > 0)
+ {
+ whitelistArray.sort();
+ for (var i = 0; i < whitelistArray.length; i++)
+ {
+ var domain = whitelistArray[i];
+ E("whitelisting-table").appendChild(createWhitelistElem(domain));
+ }
+ }
+ });
+ }
+ });
+
+ loadRecommendations(function(recommends)
+ {
+ ext.backgroundPage.sendMessage({
+ type: "subscriptions.get",
+ downloadable: true
+ }, function(subscriptions)
+ {
+ getAcceptableAdsURL(function(url)
+ {
+ acceptableAdsUrl = url;
+ for (var i = 0; i < subscriptions.length; i++)
+ {
+ if (subscriptions[i].url == acceptableAdsUrl)
+ {
+ E("allow-whitelist-cb").checked = !subscriptions[i].disabled;
+ continue;
+ }
+
+ var subscription = recommends[subscriptions[i].url];
+ if (!subscription)
+ recommends[subscriptions[i].url] = subscriptions[i];
+ else
+ {
+ subscription.disabled = subscriptions[i].disabled;
+ if (subscription.type == "ads")
+ subscription.isAdded = true;
+ }
+ }
+ for (var key in recommends)
+ addOptionItem(recommends[key]);
+ });
+ });
+ });
+ }
+
+ function loadRecommendations(callback)
+ {
+ var recommendations = {};
+ var request = new XMLHttpRequest();
+ request.open("GET", "subscriptions.xml");
Thomas Greiner 2015/01/30 13:55:52 This function call is missing the third parameter
saroyanm 2015/02/13 10:57:11 Done.
+ request.onload = function()
+ {
+ var list = document.getElementById("subscriptionSelector");
+ var elements = request.responseXML.documentElement.getElementsByTagName("subscription");
+ for (var i = 0; i < elements.length; i++)
+ {
+ var element = elements[i];
+ var subscription = {};
Thomas Greiner 2015/01/30 13:55:52 Use `var subscription = Object.create(null);` inst
saroyanm 2015/02/13 10:57:11 Done.
+ subscription.title = element.getAttribute("title");
+ subscription.url = element.getAttribute("url");
+ subscription.disabled = true;
+ var prefix = element.getAttribute("prefixes");
+ if (prefix)
+ {
+ subscription.prefixes = element.getAttribute("prefixes");
+ subscription.type = "ads";
+ subscription.display = ext.i18n.getMessage("options_language_"+subscription.prefixes.replace(/,/g, '_'));
Thomas Greiner 2015/01/30 13:55:52 This line doesn't need to be that long. I'd sugges
saroyanm 2015/02/13 10:57:11 Done.
+ }
+ else
+ subscription.display = element.getAttribute("specialization");
+
+ var popular = element.getAttribute("popular");
Thomas Greiner 2015/01/30 13:55:52 It's safer to convert this string into a boolean.
saroyanm 2015/02/13 10:57:11 Done.
+ if (popular)
+ subscription.popular = element.getAttribute("popular");
+
+ recommendations[subscription.url] = subscription;
+ }
+ optionSubscriptions = recommendations;
+ callback(recommendations);
+ }
+ request.send();
+ }
+
+ function searchLanguage()
+ {
+ var searchVal = this.value;
+ var items = E("all-lang-table").childNodes;
+ for (var i = 0; i < items.length; ++i)
+ {
+ var item = items[i];
+ var language = item.getElementsByTagName("span")[1].innerHTML;
Thomas Greiner 2015/01/30 13:55:52 Don't rely on the content of an element because it
+ if (language.toLowerCase().indexOf(searchVal.toLowerCase()) > -1)
+ item.style.display = "block";
+ else
+ item.style.display = "none";
+ }
+ }
+
+ function addOptionItem(subscription)
+ {
+ var display = subscription.display ? subscription.display : subscription.title;
Thomas Greiner 2015/01/30 13:55:52 You can reduce this down to `var display = subscri
+ var getPossition = function(elements, subscription)
Thomas Greiner 2015/01/30 13:55:52 This variable name is spelled incorrectly.
Thomas Greiner 2015/01/30 13:55:52 You were able to use a much nicer approach for pop
saroyanm 2015/02/13 10:57:11 Changed to use arrays for sorting.
+ {
+ var localArray = [];
+ for (var i = 0; i < elements.length; i++)
+ {
+ var elem = elements[i];
+ localArray.push(elem);
+ }
+
+ localArray.push(subscription);
+ return localArray.sort(function(a, b) {
+ var aPopular = a.getElementsByClassName("popular").length > 0;
+ var bPopular = b.getElementsByClassName("popular").length > 0;
+ if(aPopular == bPopular)
+ {
+ var aValue = a.getElementsByClassName("display")[0].innerHTML.toLowerCase();
+ var bValue = b.getElementsByClassName("display")[0].innerHTML.toLowerCase();
+ if (aValue < bValue)
+ return -1;
+ if (aValue > bValue)
+ return 1;
+ return 0;
+ }
+ if (aPopular == "true")
+ return 1;
+ else
+ return -1;
+ }).indexOf(subscription);
+ };
+
+ var checkBoxClick = function(e)
+ {
+ e.preventDefault();
+ toggleSubscription(subscription);
+ };
+
+ var appendToTable = function(table, elem)
+ {
+ var elements = table.getElementsByTagName("li");
+ if (elements.length == 0)
+ table.appendChild(elem);
+ else
+ {
+ var possition = getPossition(elements, elem);
Thomas Greiner 2015/01/30 13:55:52 This variable name is spelled incorrectly.
+ table.insertBefore(elem, table.childNodes[possition]);
+ }
+ };
+
+ if (subscription.type && subscription.type == "ads")
+ {
+ if (!subscription.isAdded)
+ {
+ var listElem = generateListElement(subscription, subscription.display, "add");
+ listElem.dataset.url = subscription.url;
+ listElem._subscription = subscription;
+ listElem.getElementsByTagName("button")[0].addEventListener("click", function(e)
+ {
+ addSubscription(this.dataset.url);
+ }.bind(listElem), false);
+ appendToTable(E("all-lang-table"), listElem);
+ }
+ else
+ {
+ var listElem = generateListElement(subscription, display, "checkbox");
+ listElem.dataset.url = subscription.url;
+ listElem._subscription = subscription;
+ listElem.getElementsByTagName("input")[0].addEventListener("click", checkBoxClick, false);
+ appendToTable(E("blocking-languages-table"), listElem);
+ var listElem = generateListElement(subscription, display);
+ listElem.dataset.url = subscription.url;
+ listElem._subscription = subscription;
+ appendToTable(E("blocking-languages-modal-table"), listElem);
+ }
+ }
+ else
+ {
+ var listElem = generateListElement(subscription, display, "checkbox");
+ listElem.dataset.url = subscription.url;
+ listElem._subscription = subscription;
+ listElem.getElementsByTagName("input")[0].addEventListener("click", checkBoxClick, false);
+ appendToTable(E("further-list-table"), listElem);
+ }
+ }
+
+ function addLanguageSubscription(subscription)
+ {
+ var optionSubscription = getOptionSubscription(subscription.url);
+ var elems = getElementsByUrl(subscription.url);
+ for (var i = 0; i < elems.length; i++)
Thomas Greiner 2015/01/30 13:55:52 Why are you removing elements when adding a subscr
saroyanm 2015/02/13 10:57:11 The item will stay there when it's added while we
+ elems[i].parentNode.removeChild(elems[i]);
+ optionSubscription.isAdded = true;
+ optionSubscription.disabled = false;
+ addOptionItem(optionSubscription);
+ }
+
+ function createWhitelistElem(domain)
+ {
+ var listElem = generateListElement(null, domain, "delete");
+ listElem.dataset.domain = domain;
+ listElem.getElementsByTagName("button")[0].addEventListener("click", removeWhitelistBtnClick.bind(listElem), false);
+ return listElem;
+ }
+
+ function addFurtherList(subscription)
Thomas Greiner 2015/01/30 13:55:52 We tend to call them "custom subscriptions" in our
saroyanm 2015/02/13 10:57:11 Done.
+ {
+ var optionSubscription = getOptionSubscription(subscription.url);
+ if (optionSubscription)
+ {
+ optionSubscription.disabled = false;
+ addOptionItem(optionSubscription);
+ }
+ else
+ {
+ optionSubscriptions[subscription.url] = subscription;
+ addOptionItem(subscription);
+ }
+ }
+
+ function updateSubscriptionState(subscription, state)
+ {
+ var elem = getElementsByUrl(subscription.url);
+ if (elem.length > 0)
+ {
+ for (var i = 0; i < elem.length; i++)
Thomas Greiner 2015/01/30 13:55:52 A subscription should only be shown once on the pa
saroyanm 2015/02/13 10:57:11 if you mean tab, then Language subscriptions shown
+ {
+ var checkbox = elem[i].getElementsByTagName("input")[0];
+ if (checkbox)
+ checkbox.checked = state;
Thomas Greiner 2015/01/30 13:55:52 Might make sense to implement some simple data-bin
+ }
+ }
+ else
+ {
+ if (subscription.url == acceptableAdsUrl)
+ E("allow-whitelist-cb").checked = state;
+ else
+ addFurtherList(subscription);
+ }
+ }
+
+ function getElementsByUrl(url)
+ {
+ return document.querySelectorAll("[data-url='"+url+"']");
+ }
+
+ function generateListElement(subscription, text, type)
+ {
+ var list = document.createElement("li");
+ if (type == "checkbox")
+ {
+ var input = document.createElement("input");
+ input.setAttribute("type", "checkbox");
+ if (subscription.disabled == false)
+ input.checked = true;
+ list.appendChild(input);
+ }
+ else if (type == "delete")
+ {
+ var button = document.createElement("button");
+ button.setAttribute("class", "delete");
+ list.appendChild(button);
+ }
+ else if (type == "add")
+ {
+ var button = document.createElement("button");
+ button.setAttribute("class", "button-add");
+ var span = document.createElement("span");
+ span.innerHTML = "+" + ext.i18n.getMessage("options_btn_add");
+ button.appendChild(span);
+ list.appendChild(button);
+ }
+ var span = document.createElement("span");
+ span.setAttribute("class", "display");
+ span.innerHTML = text;
+ list.appendChild(span);
+
+ if (subscription && subscription.popular == "true")
+ {
+ var popular = document.createElement("span");
+ popular.setAttribute("class", "popular");
+ popular.innerHTML = "popular";
+ list.appendChild(popular);
+ }
+
+ return list;
+ }
+
+ function getOptionSubscription(url)
+ {
+ return optionSubscriptions[url];
+ }
+
+ function importListBtnCLick()
+ {
+ var url = E("blockingList-textbox").value;
+ addSubscription(url);
+ Modal.close();
+ }
+
+ function whitelistDomainBtnClick()
+ {
+ var domain = E("whitelisting-textbox").value;
+ if (domain)
+ addWhitelistedDomain(domain);
+ }
+
+ function removeWhitelistBtnClick()
+ {
+ removeWhitelistedDomain(this.dataset.domain);
+ }
+
+ function editOwnRulsBtnClick()
+ {
+
+ }
+
+ function showAddSubscriptionDialog(action, subscription)
Thomas Greiner 2015/01/30 13:55:52 The `action` parameter is unused.
saroyanm 2015/02/13 10:57:11 Done.
+ {
+ E("blockingList-textbox").value = subscription.url;
+ Modal.open("further-blocking-modal");
+ }
+
+ function getAcceptableAdsURL(callback)
+ {
+ ext.backgroundPage.sendMessage({
+ type: "prefs.get",
+ key: "subscriptions_exceptionsurl"
+ }, function(value)
+ {
+ getAcceptableAdsURL = function(callback)
+ {
+ callback(value);
+ }
+ getAcceptableAdsURL(callback);
+ });
+ }
+
+ function toggleSubscription(subscription)
+ {
+ ext.backgroundPage.sendMessage({
+ type: "subscriptions.toggle",
+ url: subscription.url,
+ title: subscription.title,
+ homepage: subscription.homepage
+ });
+ }
+
+ function toggleAcceptableAds(e)
+ {
+ e.preventDefault();
+ var acceptableCheckbox = this;
Thomas Greiner 2015/01/30 13:55:52 This variable is not necessary. You can access the
+ getAcceptableAdsURL(function(url)
+ {
+ var isChecked = !acceptableCheckbox.checked;
+ var title = "Allow non-intrusive advertising";
Thomas Greiner 2015/01/30 13:55:52 Those two values are only used once so no need to
+ if (isChecked)
+ removeSubscription(url);
+ else
+ addSubscription(url, title);
+ });
+ }
+
+ function addSubscription(url, title, homepage)
+ {
+ var message = {
+ type: "subscriptions.add",
+ url: url
+ };
+ if (title)
+ message.title = title;
+ if (homepage)
+ message.homepage = homepage;
+
+ ext.backgroundPage.sendMessage(message);
+ }
+
+ function removeSubscription(url)
+ {
+ ext.backgroundPage.sendMessage({
+ type: "subscriptions.remove",
+ url: url
+ });
+ }
+
+ function addWhitelistedDomain(domain)
+ {
+ ext.backgroundPage.sendMessage({
+ type: "filters.add",
+ text: "@@||" + domain.toLowerCase() + "^$document"
+ });
+ }
+
+ function removeWhitelistedDomain(domain)
+ {
+ ext.backgroundPage.sendMessage({
+ type: "filters.remove",
+ text: "@@||" + domain.toLowerCase() + "^$document"
+ });
+ }
+
+ function onFilterMessage(action, filter)
+ {
+ switch (action)
+ {
+ case "added":
+ var match = filter.text.match(/^@@\|\|([^\/:]+)\^\$document$/);
+ if (match[1])
Thomas Greiner 2015/01/30 13:55:52 `match` could be null which can cause an exception
saroyanm 2015/02/13 10:57:11 Done.
+ {
+ var whitelistTbl = E("whitelisting-table");
+ var items = whitelistTbl.getElementsByClassName("display");
+ var domains = [];
+ for (var i = 0; i < items.length; i++)
+ {
+ domains.push(items[i].innerHTML);
Thomas Greiner 2015/01/30 13:55:52 The content of an HTML element might change so bet
saroyanm 2015/02/13 10:57:11 Done.
+ }
+ var domain = match[1];
+ domains.push(domain);
+ domains.sort();
+
+ whitelistTbl.insertBefore(createWhitelistElem(domain), whitelistTbl.childNodes[domains.indexOf(domain)]);
+ E("whitelisting-textbox").value = "";
+ }
+ else
+ {
+ // TODO: add `filters[i].text` to list of custom filters
+ }
+ break;
+ case "loaded":
+ populateLists();
+ break;
+ case "removed":
+ var match = filter.text.match(/^@@\|\|([^\/:]+)\^\$document$/);
+ if (match[1])
Thomas Greiner 2015/01/30 13:55:52 As mentioned above, `match` could be null so bette
saroyanm 2015/02/13 10:57:11 Done.
+ {
+ var elem = document.querySelector("[data-domain='"+match[1]+"']");
Thomas Greiner 2015/01/30 13:55:52 We're not restricted by bandwidth so you don't nee
Thomas Greiner 2015/01/30 13:55:52 Nit: Missing spaces around `+`s
+ elem.parentNode.removeChild(elem);
+ }
+ break;
+ }
+ }
+
+ function onSubscriptionMessage(action, subscription)
+ {
+ switch (action)
+ {
+ case "added":
+ var optionSubscription = getOptionSubscription(subscription.url);
+ if (optionSubscription)
+ {
+ var isAdsType = optionSubscription.type && optionSubscription.type == "ads";
+ if (isAdsType && !optionSubscription.isAdded)
+ addLanguageSubscription(subscription);
+ else
+ updateSubscriptionState(subscription, true);
+ }
+ else if (subscription.url == acceptableAdsUrl)
+ updateSubscriptionState(subscription, true);
+ else
+ addFurtherList(subscription);
+ break;
+ case "disabled":
+ updateSubscriptionState(subscription, false);
+ break;
+ case "homepage":
+ // TODO: NYI
+ break;
+ case "removed":
+ updateSubscriptionState(subscription, false);
+ break;
+ case "title":
+ // TODO: NYI
+ break;
+ }
+ }
+
+ function updateShareLink()
+ {
+ ext.backgroundPage.sendMessage({
+ type: "filters.blocked",
+ url: "https://platform.twitter.com/widgets/",
+ requestType: "SCRIPT",
+ docDomain: "adblockplus.org",
+ thirdParty: true
+ }, function(blocked)
+ {
+ // TODO: modify "share" link accordingly
+ });
+ }
+
+ function updateVersionNumber()
+ {
+ ext.backgroundPage.sendMessage({
+ method: "app.get",
Thomas Greiner 2015/01/30 13:55:52 Nit: Adjust indentation level
saroyanm 2015/02/13 10:57:11 Done.
+ what: "addonVersion"
+ }, function(addonVersion)
+ {
+ E("abp-version").innerHTML = addonVersion;
Thomas Greiner 2015/01/30 13:55:52 You're only changing the text content so use `text
saroyanm 2015/02/13 10:57:11 Done.
+ });
+ }
+
+ function getDocLink(link, callback)
+ {
+ ext.backgroundPage.sendMessage({
+ type: "app.get",
+ what: "doclink",
+ link: link
+ }, callback);
+ }
+
+ function setLinks(id)
Thomas Greiner 2015/01/30 13:55:52 Seems like you should be able to get rid of this f
+ {
+ var element = E(id);
+ if (!element)
+ {
+ return;
+ }
+
+ var links = element.getElementsByTagName("a");
+
+ for (var i = 0; i < links.length; i++)
+ {
+ if (typeof arguments[i + 1] == "string")
+ {
+ links[i].href = arguments[i + 1];
+ links[i].setAttribute("target", "_blank");
+ }
+ else if (typeof arguments[i + 1] == "function")
+ {
+ links[i].href = "javascript:void(0);";
+ links[i].addEventListener("click", arguments[i + 1], false);
+ }
+ }
+ }
+
+ function E(id)
+ {
+ return document.getElementById(id);
+ }
+
+ ext.onMessage.addListener(function(message)
+ {
+ switch (message.type)
+ {
+ case "app.listen":
+ if (message.action == "addSubscription")
+ {
+ message.args.unshift(message.action);
Thomas Greiner 2015/01/30 13:55:52 You don't even use `message.action` in `showAddSub
saroyanm 2015/02/13 10:57:11 Done.
+ showAddSubscriptionDialog.apply(null, message.args);
Thomas Greiner 2015/01/30 13:55:52 `showAddSubscription` only requires the first argu
saroyanm 2015/02/13 10:57:11 Done.
+ }
+ break;
+ case "filters.listen":
+ message.args.unshift(message.action);
+ onFilterMessage.apply(null, message.args);
Thomas Greiner 2015/01/30 13:55:52 `onFilterMessage` only expects two parameters so l
saroyanm 2015/02/13 10:57:11 Done.
+ break;
+ case "subscriptions.listen":
+ message.args.unshift(message.action);
+ onSubscriptionMessage.apply(null, message.args);
+ break;
+ }
+ });
+
+ ext.backgroundPage.sendMessage({
+ type: "app.listen",
+ filter: ["addSubscription"]
+ });
+ ext.backgroundPage.sendMessage({
+ type: "filters.listen",
+ filter: ["added", "loaded", "removed"]
+ });
+ ext.backgroundPage.sendMessage({
+ type: "subscriptions.listen",
+ filter: ["added", "disabled", "homepage", "removed", "title"]
+ });
+
+ window.addEventListener("DOMContentLoaded", onDOMLoaded, false);
+})();

Powered by Google App Engine
This is Rietveld