pidgin/nest

Updates to plugin table

2020-01-01, Jason
520a5548f7ac
Parents d9c0224f853a
Children 0dd4cd42bec1
Updates to plugin table

* In a noscript env, hide JS dependent markup
* Temp: plugin type selection markup now in hugo tmeplate instead of JS.
Maybe we should keep to JS markup?
* Overhaul plugin table logic:
* Add support for query params and create an on page link
* Use jQuery more, seeing as we have the lib
* Add documentation
* Use `var` and non-lambdas for compatability
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hugo/data/pluginTypes.json Wed Jan 01 22:12:52 2020 +0000
@@ -0,0 +1,50 @@
+[
+ {
+ "type": "Protocol",
+ "i18n": {
+ "en": "Protocol"
+ }
+ },
+ {
+ "type": "Security and Privacy",
+ "i18n": {
+ "en": "Security and Privacy"
+ }
+ },
+ {
+ "type": "Notifications",
+ "i18n": {
+ "en": "Notifications"
+ }
+ },
+ {
+ "type": "Profile and Status Updates",
+ "i18n": {
+ "en": "Profile and Status Updates"
+ }
+ },
+ {
+ "type": "Now Playing",
+ "i18n": {
+ "en": "Now Playing"
+ }
+ },
+ {
+ "type": "Interface Tweaks",
+ "i18n": {
+ "en": "Interface Tweaks"
+ }
+ },
+ {
+ "type": "Accounts and Logs",
+ "i18n": {
+ "en": "Accounts and Logs"
+ }
+ },
+ {
+ "type": "Miscellaneous",
+ "i18n": {
+ "en": "Miscellaneous"
+ }
+ }
+]
\ No newline at end of file
--- a/hugo/layouts/shortcodes/plugintable.html Wed Jan 01 03:34:44 2020 +0000
+++ b/hugo/layouts/shortcodes/plugintable.html Wed Jan 01 22:12:52 2020 +0000
@@ -20,7 +20,16 @@
<li>{{ template "community" }} {{ T "Community-Info" }}</li>
</ul>
-<div id="plugin-filters">
+{{/* No point in showing search filters if there's no JS */}}
+<noscript>
+ <style>
+ #plugin-filters, #search-link-wrap {
+ display: none;
+ }
+ </style>
+</noscript>
+
+<div id="plugin-filters" class="hidden">
<div id="publisher-selector">
<label style="display: inline-block;">
<input type="radio" id="all" name="publisher" style="margin-right: 0; margin-left: 0.75rem;" value="all" checked>
@@ -43,7 +52,14 @@
<input type="text" id="plugin-filter-search" placeholder="{{ T "Search-placeholder" }}" />
- <div id="plugin-selector"></div>
+ <div id="plugin-selector">
+ {{ range .Site.Data.pluginTypes }}
+ <label class="pidgin-plugin-filter-checkbox">
+ <input type="checkbox" data-type="{{ .type }}">
+ {{ .i18n.en }}
+ </label>
+ {{ end }}
+ </div>
</div>
<table id="plugin-table">
@@ -70,6 +86,7 @@
{{ .heading }}
{{ end }}
</td>
+
<td class="plugin-publisher">
{{ if eq true .isTrusted }}
{{ template "trusted" }}
@@ -94,10 +111,16 @@
</tbody>
</table>
+
+<p id="search-link-wrap" class="hidden">
+ Share this search:<br />
+ <a id="search-link"></a>
+</p>
+
<script src="/js/plugin-table.js"></script>
<style>
- .filter-hide {
+ .hidden {
display: none;
}
@@ -120,4 +143,9 @@
margin: 8px;
display: inline-block;
}
+
+ #search-link-wrap {
+ font-size: small;
+ text-align: center;
+ }
</style>
--- a/hugo/static/js/plugin-table.js Wed Jan 01 03:34:44 2020 +0000
+++ b/hugo/static/js/plugin-table.js Wed Jan 01 22:12:52 2020 +0000
@@ -1,96 +1,158 @@
-document.addEventListener("DOMContentLoaded", () => {
- const selectorsContainer = document.getElementById("plugin-selector");
- const search = document.getElementById("plugin-filter-search");
- search.addEventListener("input", debounce(1000 * 0.5, filterRows));
- const [, ...rows] = document
- .getElementById("plugin-table")
- .getElementsByTagName("tr");
- const types = {};
- const typeFilter = new Set();
- const rowinfo = rows.map(elem => {
- const type = elem.dataset.type;
- if (type) {
- if (!types[type]) types[type] = [];
+/**
+ * Array representing each plugin row
+ * @type {{
+ * elem: HTMLElement,
+ * head: string,
+ * info: string,
+ * repo: string,
+ * isTrusted: boolean
+ * }[]}
+ */
+var pluginRow;
+/**
+ * String Search element
+ * @type {HTMLInputElement}
+ */
+var searchTextBox;
- types[type].push(elem);
- }
+var url = new URL(window.location);
+var params = url.searchParams;
+var searchState = {
+ /**
+ * @type {"All" | "Trusted" | "Community"}
+ */
+ publisher: params.get("publisher") || "all",
+ /**
+ * String to search for
+ */
+ query: params.get("query") || "",
+ /**
+ * What type of plugin has been selected
+ */
+ type: new Set(
+ params.has("type")
+ ? params
+ .get("type")
+ .split(",")
+ .map(function(str) {
+ return str.trim();
+ })
+ : []
+ ),
+};
+
+document.addEventListener("DOMContentLoaded", function() {
+ // Iterate through rows and populate
+ pluginRow = Array.from($("#plugin-table>tbody>tr")).map(function(elem) {
+ var contents = function(selectorQuery) {
+ return $(elem)
+ .find(selectorQuery)
+ .text()
+ .trim();
+ };
return {
elem,
- head: getContents("plugin-heading", elem),
- info: getContents("plugin-info", elem),
- repo: getContents("plugin-repo", elem),
+ head: contents(".plugin-heading"),
+ info: contents(".plugin-info"),
+ repo: contents(".plugin-repo"),
isTrusted: elem.getAttribute("isTrusted") == "true",
};
});
- document
- .getElementById("publisher-selector")
- .addEventListener("click", filterRows);
+ // Add click handler to All|Trusted|Community selector
+ $("#publisher-selector").click(updateFilters);
+ searchTextBox = document.querySelector("#plugin-filter-search");
+ searchTextBox.addEventListener(
+ "input",
+ debounce(1000 * 0.5, updateFilters)
+ );
- Object.keys(types).forEach(type => {
- const label = createAndAppend("label", selectorsContainer);
- label.classList.add("pidgin-plugin-filter-checkbox");
- const input = createAndAppend("input", label);
- input.type = "checkbox";
- input.dataset.type = type;
- input.addEventListener("click", clickEvent);
- label.appendChild(document.createTextNode(type));
+ // Set up plugin type selector interface
+ Array.from(
+ document.querySelectorAll("#plugin-selector>label>input")
+ ).forEach(function(elem) {
+ elem.addEventListener("click", pluginTypeSelectorClickEvent);
});
- /////////////////////////
+ updateFilters();
+ $("#plugin-filters,#search-link-wrap").removeClass("hidden");
+});
+
+/**
+ * Event handler for plugin type checkboxes
+ * @param {MouseEvent} event
+ * @param {HTMLInputElement} event.target
+ */
+function pluginTypeSelectorClickEvent(target) {
+ searchState.type[target.checked ? "add" : "delete"](target.dataset.type);
+ updateFilters();
+}
- function getContents(className, elem) {
- return elem
- .getElementsByClassName(className)[0]
- .textContent.toLowerCase();
- }
+/**
+ * Visually filters Plugins by toggling visibility
+ */
+function updateFilters() {
+ searchState.query = (searchTextBox.value || "").toLowerCase();
+ searchState.publisher = $('input[name="publisher"]:checked')[0].value;
- function clickEvent({ target }) {
- typeFilter[target.checked ? "add" : "delete"](target.dataset.type);
- filterRows();
+ pluginRow.forEach(function(row) {
+ if (shouldFilter(row)) {
+ row.elem.classList.remove("hidden");
+ } else {
+ row.elem.classList.add("hidden");
+ }
+ });
+
+ // Updates query string
+ params.set("publisher", searchState.publisher);
+ params.set("query", searchState.query);
+ params.set("type", Array.from(searchState.type).join(","));
+
+ var urlString = String(url);
+
+ if (history.pushState) {
+ history.pushState({ path: urlString }, "", urlString);
}
- const publisherRadioQuery = 'input[name="publisher"]:checked';
-
- function filterRows() {
- const str = (search.value || "").toLowerCase();
- const publisherSelector = document.querySelector(publisherRadioQuery)
- .value;
-
- rowinfo.forEach(row => {
- if (shouldFilter(row, publisherSelector, str))
- row.elem.classList.remove("filter-hide");
- else row.elem.classList.add("filter-hide");
- });
- }
+ var link = $("#search-link");
+ link.attr("href", urlString);
+ link.text(urlString);
+}
- function shouldFilter(row, publisher, str) {
- return (
- // Checkboxes
- (!typeFilter.size || typeFilter.has(row.elem.dataset.type)) &&
- // Provider
- (publisher == "all" ||
- (publisher == "pidgin" && row.isTrusted) ||
- (publisher == "community" && !row.isTrusted)) &&
- // Search box
- (!str ||
- row.head.includes(str) ||
- row.info.includes(str) ||
- row.repo.includes(str))
- );
- }
+/**
+ * Decides if a plugin row should be filterd
+ * @param {*} row
+ * @returns boolean
+ */
+function shouldFilter(row) {
+ return (
+ // Checkboxes
+ (!searchState.type.size ||
+ searchState.type.has(row.elem.dataset.type)) &&
+ // Provider
+ (searchState.publisher == "all" ||
+ (searchState.publisher == "pidgin" && row.isTrusted) ||
+ (searchState.publisher == "community" && !row.isTrusted)) &&
+ // Search box
+ (!searchState.query ||
+ row.head.includes(searchState.query) ||
+ row.info.includes(searchState.query) ||
+ row.repo.includes(searchState.query))
+ );
+}
- function debounce(t, fn) {
- let timer;
-
- return function(...args) {
- clearTimeout(timer);
- timer = setTimeout(() => fn(...args), t);
- };
- }
-
- function createAndAppend(tag, elem = document) {
- return elem.appendChild(document.createElement(tag));
- }
-});
+/**
+ * Simple Debounce function
+ * @param {time} time
+ * @param {function} callback
+ */
+function debounce(time, callback) {
+ var timer;
+ return function(...args) {
+ clearTimeout(timer);
+ timer = setTimeout(function() {
+ callback(...args);
+ }, time);
+ };
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/plugin-map.js Wed Jan 01 22:12:52 2020 +0000
@@ -0,0 +1,20 @@
+const fs = require("fs");
+const data = require("../hugo/data/plugins.json");
+
+const types = new Set();
+
+data.forEach(plugin => {
+ types.add(plugin.type);
+});
+
+const newData = Array.from(types).map(type => ({
+ type,
+ i18n: {
+ en: type,
+ },
+}));
+
+fs.writeFileSync(
+ "../hugo/data/pluginTypes.json",
+ JSON.stringify(newData, null, "\t")
+);