Bitcoin Forum
June 08, 2026, 10:16:26 PM *
News: Latest Bitcoin Core release: 31.0 [Torrent]
 
   Home   Help Search Login Register More  
Pages: [1]
  Print  
Author Topic: [Userscript] Watchlist A-Z Sorter -A tool to easily organize large Watchlists  (Read 69 times)
GhostOfBitcoin (OP)
Jr. Member
*
Offline

Activity: 42
Merit: 29


View Profile
Today at 09:47:29 AM
Last edit: Today at 05:14:03 PM by GhostOfBitcoin
Merited by satscraper (5)
 #1

I would like to suggest adding the new button in Edit Watchlist that allows all entries to be displayed in alphabetical order. I would appreciate it if this could be implemented.

Many people use Bitcointalk's Watchlist feature regularly. They add important topics to the watchlist, and remove old topics when they are no longer needed. But when the watchlist grows large, a common problem arises the entries are randomly placed. As a result, you have to scroll through the entire list to find a specific topic, which is time-consuming and a bit annoying.

I'm working on a small userscript to solve this problem:

Bitcointalk Watchlist A-Z Sorter

The main task of this userscript will be to add a new button to the Edit Watchlist page, like this:

Code:
Sort A-Z

Clicking the button will sort all topic entries in the watchlist alphabetically. You will have the option to restore the original order if you wish.

Why is this userscript needed?

1. It will be easier for those with large watchlists to find specific topics.
2. The hassle of manually scrolling to find topics will be reduced.
3. When removing old entries, users will be able to quickly identify topics.
4. Watchlist management will be cleaner and more organized.
5. No server-side changes to the forum will be required, as all work will be done inside the browser.

Basic technical flow:

1. The script will first check if the current page is a Bitcointalk watchlist page.
2. The page URL will be matched
3. Then the watchlist entries from the page will be detected.
4. Each entry usually has a checkbox and a topic link.
5. The script will create an entry object by taking the checkbox and topic link together.
6. The sorting text will be created from the topic title.
7. A-Z sorting will be done using JavaScript localeCompare().
8. The entries will be reordered in the DOM.
9. The original order will be kept in memory, so that the user can restore it if desired.

Sorting logic

Topic titles will be normalized for sorting:

Code:
function normalize(text) {
  return text.replace(/\s+/g, ' ').trim().toLowerCase();
}

Then it will be compared:

Code:
titleA.localeCompare(titleB, undefined, {
  numeric: true,
  sensitivity: 'base'
});

This will reduce the uppercase/lowercase issue and sorting will be relatively better even if there are numbers.

On the Edit Watchlist page, there will be a new button next to the Remove checked button:



Code:
Sort A-Z

After clicking:



Code:
Restore original order

The user can return to the original order from the sorted view if they wish.


How to Install:

Step 1: First, install a userscript manager extension in your browser (Tampermonkey).
Step 2: Go to the extension dashboard and click on Create a new script or the plus (+) icon.
Step 3: Copy and paste the entire code below and save .
Step 4: Now refresh the BitcoinTalk forum. You will see a beautiful market ticker on the right side!

GreasyFork Download


Userscript version 1.1.0 -06-09-2026 --> 05:00:03 PM

A button has been added. You can now add a custom display title name there, leaving the URL unchanged. If you give a custom name, it will be saved in localStorage. As a result, even if the watchlist page loads later, the saved custom name will be displayed instead of the original title.

Code:
// ==UserScript==
// @name         Bitcointalk Watchlist Alphabetical Sorter
// @namespace    bitcointalk-watchlist-tools
// @version      1.1.0
// @description  Adds Sort A-Z and selected-entry rename buttons on Bitcointalk's edit watchlist page.
// @author       GhostOfBitcoin
// @match        https://bitcointalk.org/watchlist.php*
// @match        http://bitcointalk.org/watchlist.php*
// @include      https://bitcointalk.org/watchlist.php*
// @include      http://bitcointalk.org/watchlist.php*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  const BUTTON_ID = 'bt-watchlist-sort-az';
  const EDIT_BUTTON_ID = 'bt-watchlist-edit-title';
  const STATUS_ID = 'bt-watchlist-sort-status';
  const STORAGE_KEY = 'bt-watchlist-custom-titles';
  const LINK_SELECTOR = 'a[href*="topic="], a[href*="board="]';

  let originalEntries = null;
  let sorted = false;

  function cleanText(value) {
    return String(value || '').replace(/\s+/g, ' ').trim();
  }

  function sortText(value) {
    return cleanText(value).toLocaleLowerCase();
  }

  function loadCustomTitles() {
    try {
      return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}') || {};
    } catch (error) {
      return {};
    }
  }

  function saveCustomTitles(titles) {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(titles));
  }

  function normalizeHref(href) {
    try {
      const url = new URL(href, location.href);
      url.hash = '';
      return url.href;
    } catch (error) {
      return String(href || '');
    }
  }

  function getEntryLink(entry) {
    for (const node of entry.nodes) {
      if (node.nodeType !== Node.ELEMENT_NODE) continue;

      if (node.matches && node.matches(LINK_SELECTOR)) return node;

      const link = node.querySelector ? node.querySelector(LINK_SELECTOR) : null;
      if (link) return link;
    }

    return null;
  }

  function applyCustomTitles() {
    const titles = loadCustomTitles();

    document.querySelectorAll(LINK_SELECTOR).forEach((link) => {
      const customTitle = titles[normalizeHref(link.href)];
      if (customTitle) link.textContent = customTitle;
    });
  }

  function isWatchlistPage() {
    return /\/watchlist\.php/i.test(location.pathname);
  }

  function isControlCheckbox(box) {
    const text = `${box.name || ''} ${box.id || ''} ${box.value || ''}`.toLowerCase();
    return text.includes('all') || text.includes('checkall') || text.includes('check_all');
  }

  function getTitleFromNode(node) {
    const link = node.querySelector ? node.querySelector(LINK_SELECTOR) : null;
    return link ? sortText(link.textContent) : '';
  }

  function nextNodeBelongsToEntry(node) {
    if (!node) return false;
    if (node.nodeType === Node.TEXT_NODE) return true;
    if (node.nodeType !== Node.ELEMENT_NODE) return false;
    if (node.matches('br')) return true;
    return false;
  }

  function collectTrailingBreaks(startNode, nodes) {
    let node = startNode.nextSibling;
    let guard = 0;

    while (nextNodeBelongsToEntry(node) && guard < 10) {
      nodes.push(node);
      if (node.nodeType === Node.ELEMENT_NODE && node.matches('br')) break;
      node = node.nextSibling;
      guard += 1;
    }
  }

  function buildEntryFromCheckbox(box, index) {
    if (isControlCheckbox(box)) return null;

    const tableRow = box.closest('tr');
    if (tableRow && tableRow.querySelector(LINK_SELECTOR)) {
      return {
        index,
        parent: tableRow.parentNode,
        title: getTitleFromNode(tableRow),
        nodes: [tableRow]
      };
    }

    const label = box.closest('label');
    if (label && label.querySelector(LINK_SELECTOR)) {
      const nodes = [label];
      collectTrailingBreaks(label, nodes);

      return {
        index,
        parent: label.parentNode,
        title: getTitleFromNode(label),
        nodes
      };
    }

    const parent = box.parentNode;
    if (!parent) return null;

    let link = null;
    let node = box.nextSibling;
    let guard = 0;

    while (node && node.parentNode === parent && guard < 30) {
      if (node.nodeType === Node.ELEMENT_NODE) {
        if (node.matches(LINK_SELECTOR)) {
          link = node;
          break;
        }

        link = node.querySelector(LINK_SELECTOR);
        if (link) break;

        if (node.matches('br') || node.matches('input[type="checkbox"]')) break;
      }

      node = node.nextSibling;
      guard += 1;
    }

    if (!link) return null;

    const nodes = [];
    node = box;
    guard = 0;

    while (node && node.parentNode === parent && guard < 40) {
      nodes.push(node);
      if (node.nodeType === Node.ELEMENT_NODE && node.matches('br')) break;

      const next = node.nextSibling;
      if (next && next.nodeType === Node.ELEMENT_NODE && next.matches('input[type="checkbox"]')) {
        break;
      }

      node = next;
      guard += 1;
    }

    return {
      index,
      parent,
      title: sortText(link.textContent),
      nodes
    };
  }

  function findEntries() {
    applyCustomTitles();

    const boxes = Array.from(document.querySelectorAll('input[type="checkbox"]'));
    const entries = boxes
      .map(buildEntryFromCheckbox)
      .filter((entry) => entry && entry.parent && entry.title && entry.nodes.length > 0);

    const groups = new Map();
    entries.forEach((entry) => {
      if (!groups.has(entry.parent)) groups.set(entry.parent, []);
      groups.get(entry.parent).push(entry);
    });

    let bestEntries = [];
    groups.forEach((groupEntries) => {
      if (groupEntries.length > bestEntries.length) bestEntries = groupEntries;
    });

    return bestEntries.length > 1 ? bestEntries : [];
  }

  function getSelectedEntries() {
    return findEntries().filter((entry) => {
      return entry.nodes.some((node) => {
        if (node.nodeType !== Node.ELEMENT_NODE) return false;

        const checkbox = node.matches('input[type="checkbox"]')
          ? node
          : node.querySelector('input[type="checkbox"]');

        return checkbox && checkbox.checked && !isControlCheckbox(checkbox);
      });
    });
  }

  function editSelectedEntryTitle() {
    const selectedEntries = getSelectedEntries();

    if (selectedEntries.length !== 1) {
      setStatus('Select exactly one entry to rename');
      return;
    }

    const link = getEntryLink(selectedEntries[0]);
    if (!link) {
      setStatus('Selected entry link was not found');
      return;
    }

    const currentTitle = cleanText(link.textContent);
    const nextTitle = window.prompt('Edit selected watchlist entry name:', currentTitle);
    if (nextTitle === null) return;

    const cleanTitle = cleanText(nextTitle);
    if (!cleanTitle) {
      setStatus('Name was not changed');
      return;
    }

    const titles = loadCustomTitles();
    titles[normalizeHref(link.href)] = cleanTitle;
    saveCustomTitles(titles);

    link.textContent = cleanTitle;
    setStatus('Selected entry name updated');
  }

  function moveEntries(entries) {
    const parent = entries[0].parent;
    const marker = document.createComment('bt-watchlist-sort-marker');
    parent.insertBefore(marker, entries[0].nodes[0]);

    entries.forEach((entry) => {
      entry.nodes.forEach((node) => parent.insertBefore(node, marker));
    });

    parent.removeChild(marker);
  }

  function sortEntries() {
    const entries = findEntries();
    if (!entries.length) {
      setStatus('No sortable entries found');
      return;
    }

    if (!originalEntries) {
      originalEntries = entries.map((entry) => ({
        parent: entry.parent,
        title: entry.title,
        nodes: entry.nodes.slice()
      }));
    }

    const sortedEntries = entries.slice().sort((a, b) => {
      return a.title.localeCompare(b.title, undefined, {
        numeric: true,
        sensitivity: 'base'
      });
    });

    moveEntries(sortedEntries);
    setStatus(`Sorted ${sortedEntries.length} entries`);
  }

  function restoreEntries() {
    if (!originalEntries) {
      setStatus('Original order is not saved yet');
      return;
    }

    moveEntries(originalEntries);
    setStatus('Original order restored');
  }

  function setStatus(message) {
    const status = document.getElementById(STATUS_ID);
    if (status) status.textContent = message;
  }

  function insertButton() {
    if (document.getElementById(BUTTON_ID)) return;

    const entries = findEntries();

    const wrapper = document.createElement('div');
    wrapper.style.margin = '10px 0';

    const button = document.createElement('button');
    button.id = BUTTON_ID;
    button.type = 'button';
    button.textContent = 'Sort A-Z';
    button.style.marginRight = '8px';
    button.style.padding = '4px 10px';
    button.style.cursor = 'pointer';

    const editButton = document.createElement('button');
    editButton.id = EDIT_BUTTON_ID;
    editButton.type = 'button';
    editButton.textContent = 'Edit selected name';
    editButton.style.marginRight = '8px';
    editButton.style.padding = '4px 10px';
    editButton.style.cursor = 'pointer';

    const status = document.createElement('span');
    status.id = STATUS_ID;
    status.style.color = '#555';
    status.textContent = entries.length ? `Found ${entries.length} entries` : 'Sorter loaded';

    button.addEventListener('click', () => {
      if (sorted) {
        restoreEntries();
        button.textContent = 'Sort A-Z';
        sorted = false;
        return;
      }

      sortEntries();
      button.textContent = 'Restore original order';
      sorted = true;
    });

    editButton.addEventListener('click', editSelectedEntryTitle);

    wrapper.appendChild(button);
    wrapper.appendChild(editButton);
    wrapper.appendChild(status);

    const removeButton = Array.from(document.querySelectorAll('input, button')).find((element) => {
      const text = `${element.value || ''} ${element.textContent || ''}`.toLowerCase();
      return text.includes('remove checked');
    });

    if (removeButton && removeButton.parentNode) {
      removeButton.parentNode.insertBefore(wrapper, removeButton.nextSibling);
      return;
    }

    const title = Array.from(document.querySelectorAll('b, h1, h2, h3, td, div')).find((element) => {
      return cleanText(element.textContent).toLowerCase() === 'edit watchlist';
    });

    if (title && title.parentNode) {
      title.parentNode.insertBefore(wrapper, title.nextSibling);
      return;
    }

    document.body.insertBefore(wrapper, document.body.firstChild);
  }

  function init() {
    if (!isWatchlistPage()) return;
    applyCustomTitles();
    insertButton();
  }

  init();
  window.setTimeout(init, 500);
  window.setTimeout(init, 1500);
})();

If you want, I can add some more useful features in the future, such as :

  • Search / Filter Box so that a specific topic can be found very easily.
  • The ability to group Boards and Topics separately  so that the watchlist will be more organized.
  • Recently Updated Topic First Option  so that the most recently updated topics are shown at the top.
Catenaccio
Sr. Member
****
Offline

Activity: 1148
Merit: 345



View Profile
Today at 10:15:48 AM
 #2

Actually with time, people will have to clean their watchlists, and large watchlists will be reduce in number of topics. I agree that your userscript is useful for people who have need of sorting watched topics but I feel differently.

If I consider a thread is helpful, I will either bookmark it on my browser or add it to my documents which can be a word file or an excel file or anything document format which can help me finding such useful threads later quickly and easily.

Personally I don't have need of sorting A-Z topics in my watchlist because it's actually not too large.

R


▀▀▀▀▀▀▀██████▄▄
████████████████
▀▀▀▀█████▀▀▀█████
████████▌███▐████
▄▄▄▄█████▄▄▄█████
████████████████
▄▄▄▄▄▄▄██████▀▀
LLBIT|
4,000+ GAMES
███████████████████
██████████▀▄▀▀▀████
████████▀▄▀██░░░███
██████▀▄███▄▀█▄▄▄██
███▀▀▀▀▀▀█▀▀▀▀▀▀███
██░░░░░░░░█░░░░░░██
██▄░░░░░░░█░░░░░▄██
███▄░░░░▄█▄▄▄▄▄████
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
█████████
▀████████
░░▀██████
░░░░▀████
░░░░░░███
▄░░░░░███
▀█▄▄▄████
░░▀▀█████
▀▀▀▀▀▀▀▀▀
█████████
░░░▀▀████
██▄▄▀░███
█░░█▄░░██
░████▀▀██
█░░█▀░░██
██▀▀▄░███
░░░▄▄████
▀▀▀▀▀▀▀▀▀
|||
▄▄████▄▄
▀█▀
▄▀▀▄▀█▀
▄░░▄█░██░█▄░░▄
█░▄█░▀█▄▄█▀░█▄░█
▀▄░███▄▄▄▄███░▄▀
▀▀█░░░▄▄▄▄░░░█▀▀
░░██████░░█
█░░░░▀▀░░░░█
▀▄▀▄▀▄▀▄▀▄
▄░█████▀▀█████░▄
▄███████░██░███████▄
▀▀██████▄▄██████▀▀
▀▀████████▀▀
.
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
░▀▄░▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄░▄▀
███▀▄▀█████████████████▀▄▀
█████▀▄░▄▄▄▄▄███░▄▄▄▄▄▄▀
███████▀▄▀██████░█▄▄▄▄▄▄▄▄
█████████▀▄▄░███▄▄▄▄▄▄░▄▀
███████████░███████▀▄▀
███████████░██▀▄▄▄▄▀
███████████░▀▄▀
████████████▄▀
███████████
▄▄███████▄▄
▄████▀▀▀▀▀▀▀████▄
▄███▀▄▄███████▄▄▀███▄
▄██▀▄█▀▀▀█████▀▀▀█▄▀██▄
▄██▀▄███░░░▀████░███▄▀██▄
███░████░░░░░▀██░████░███
███░████░█▄░░░░▀░████░███
███░████░███▄░░░░████░███
▀██▄▀███░█████▄░░███▀▄██▀
▀██▄▀█▄▄▄██████▄██▀▄██▀
▀███▄▀▀███████▀▀▄███▀
▀████▄▄▄▄▄▄▄████▀
▀▀███████▀▀
OFFICIAL PARTNERSHIP
SOUTHAMPTON FC
FAZE CLAN
SSC NAPOLI
satscraper
Legendary
*
Offline

Activity: 1484
Merit: 2766



View Profile
Today at 10:32:26 AM
Merited by GhostOfBitcoin (1)
 #3

~


Thanks for implementing this.

I have one more suggestion regarding it.

A lot of entries in the watch list begin with insignificant words like ANN, INFO, etc., which add unnecessary distraction.

Could you add the button to edit the name of selected entry while preserving its URL? This would make the list more clear and easier to read.

▄▄███████████████████▄▄
▄███████████████████████▄
████████████████████████
█████████████████████████
████████████████████████
████████████▀██████▀████
████████████████████████
█████████▄▄▄▄███████████
██████████▄▄▄████████████
████████████████████████
████████████████▀▀███████
▀███████████████████████▀
▀▀███████████████████▀▀
 
 EARNBET 
██
██
██
██
██
██
██
██
██
██
██
██
██
███████▄▄███████████
████▄██████████████████
██▀▀███████████████▀▀███
▄████████████████████████
▄▄████████▀▀▀▀▀████████▄▄██
███████████████████████████
█████████▌██▀████████████
███████████████████████████
▀▀███████▄▄▄▄▄█████████▀▀██
▀█████████████████████▀██
██▄▄███████████████▄▄███
████▀██████████████████
███████▀▀███████████
██
██
██
██
██
██
██
██
██
██
██
██
██


▄▄▄
▄▄▄███████▐███▌███████▄▄▄
█████████████████████████
▀████▄▄▄███████▄▄▄████▀
█████████████████████
▐███████████████████▌
███████████████████
███████████████████
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀

 King of The Castle 
 $200,000 in prizes
██
██
██
██
██
██
██
██
██
██
██
██
██

 62.5% 

 
RAKEBACK
BONUS
GhostOfBitcoin (OP)
Jr. Member
*
Offline

Activity: 42
Merit: 29


View Profile
Today at 05:00:03 PM
 #4


A lot of entries in the watch list begin with insignificant words like ANN, INFO, etc., which add unnecessary distraction.


Good suggestion. Many topic titles in Watchlist actually start with prefixes like [ANN], [INFO], [BOUNTY], [DISCUSSION], etc., so when sorting A-Z, sometimes the sorting is done by prefix instead of useful title.


Could you add the button to edit the name of selected entry while preserving its URL?

A button has been added as per your suggestion. You can now add a custom display title name there, leaving the URL unchanged. If you give a custom name, it will be saved in localStorage. As a result, even if the watchlist page loads later, the saved custom name will be displayed instead of the original title.

GreasyFork Download

Userscript version 1.1.0 -06-09-2026 --> 05:00:03 PM
Code:
// ==UserScript==
// @name         Bitcointalk Watchlist Alphabetical Sorter
// @namespace    bitcointalk-watchlist-tools
// @version      1.1.0
// @description  Adds Sort A-Z and selected-entry rename buttons on Bitcointalk's edit watchlist page.
// @author       GhostOfBitcoin
// @match        https://bitcointalk.org/watchlist.php*
// @match        http://bitcointalk.org/watchlist.php*
// @include      https://bitcointalk.org/watchlist.php*
// @include      http://bitcointalk.org/watchlist.php*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  const BUTTON_ID = 'bt-watchlist-sort-az';
  const EDIT_BUTTON_ID = 'bt-watchlist-edit-title';
  const STATUS_ID = 'bt-watchlist-sort-status';
  const STORAGE_KEY = 'bt-watchlist-custom-titles';
  const LINK_SELECTOR = 'a[href*="topic="], a[href*="board="]';

  let originalEntries = null;
  let sorted = false;

  function cleanText(value) {
    return String(value || '').replace(/\s+/g, ' ').trim();
  }

  function sortText(value) {
    return cleanText(value).toLocaleLowerCase();
  }

  function loadCustomTitles() {
    try {
      return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}') || {};
    } catch (error) {
      return {};
    }
  }

  function saveCustomTitles(titles) {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(titles));
  }

  function normalizeHref(href) {
    try {
      const url = new URL(href, location.href);
      url.hash = '';
      return url.href;
    } catch (error) {
      return String(href || '');
    }
  }

  function getEntryLink(entry) {
    for (const node of entry.nodes) {
      if (node.nodeType !== Node.ELEMENT_NODE) continue;

      if (node.matches && node.matches(LINK_SELECTOR)) return node;

      const link = node.querySelector ? node.querySelector(LINK_SELECTOR) : null;
      if (link) return link;
    }

    return null;
  }

  function applyCustomTitles() {
    const titles = loadCustomTitles();

    document.querySelectorAll(LINK_SELECTOR).forEach((link) => {
      const customTitle = titles[normalizeHref(link.href)];
      if (customTitle) link.textContent = customTitle;
    });
  }

  function isWatchlistPage() {
    return /\/watchlist\.php/i.test(location.pathname);
  }

  function isControlCheckbox(box) {
    const text = `${box.name || ''} ${box.id || ''} ${box.value || ''}`.toLowerCase();
    return text.includes('all') || text.includes('checkall') || text.includes('check_all');
  }

  function getTitleFromNode(node) {
    const link = node.querySelector ? node.querySelector(LINK_SELECTOR) : null;
    return link ? sortText(link.textContent) : '';
  }

  function nextNodeBelongsToEntry(node) {
    if (!node) return false;
    if (node.nodeType === Node.TEXT_NODE) return true;
    if (node.nodeType !== Node.ELEMENT_NODE) return false;
    if (node.matches('br')) return true;
    return false;
  }

  function collectTrailingBreaks(startNode, nodes) {
    let node = startNode.nextSibling;
    let guard = 0;

    while (nextNodeBelongsToEntry(node) && guard < 10) {
      nodes.push(node);
      if (node.nodeType === Node.ELEMENT_NODE && node.matches('br')) break;
      node = node.nextSibling;
      guard += 1;
    }
  }

  function buildEntryFromCheckbox(box, index) {
    if (isControlCheckbox(box)) return null;

    const tableRow = box.closest('tr');
    if (tableRow && tableRow.querySelector(LINK_SELECTOR)) {
      return {
        index,
        parent: tableRow.parentNode,
        title: getTitleFromNode(tableRow),
        nodes: [tableRow]
      };
    }

    const label = box.closest('label');
    if (label && label.querySelector(LINK_SELECTOR)) {
      const nodes = [label];
      collectTrailingBreaks(label, nodes);

      return {
        index,
        parent: label.parentNode,
        title: getTitleFromNode(label),
        nodes
      };
    }

    const parent = box.parentNode;
    if (!parent) return null;

    let link = null;
    let node = box.nextSibling;
    let guard = 0;

    while (node && node.parentNode === parent && guard < 30) {
      if (node.nodeType === Node.ELEMENT_NODE) {
        if (node.matches(LINK_SELECTOR)) {
          link = node;
          break;
        }

        link = node.querySelector(LINK_SELECTOR);
        if (link) break;

        if (node.matches('br') || node.matches('input[type="checkbox"]')) break;
      }

      node = node.nextSibling;
      guard += 1;
    }

    if (!link) return null;

    const nodes = [];
    node = box;
    guard = 0;

    while (node && node.parentNode === parent && guard < 40) {
      nodes.push(node);
      if (node.nodeType === Node.ELEMENT_NODE && node.matches('br')) break;

      const next = node.nextSibling;
      if (next && next.nodeType === Node.ELEMENT_NODE && next.matches('input[type="checkbox"]')) {
        break;
      }

      node = next;
      guard += 1;
    }

    return {
      index,
      parent,
      title: sortText(link.textContent),
      nodes
    };
  }

  function findEntries() {
    applyCustomTitles();

    const boxes = Array.from(document.querySelectorAll('input[type="checkbox"]'));
    const entries = boxes
      .map(buildEntryFromCheckbox)
      .filter((entry) => entry && entry.parent && entry.title && entry.nodes.length > 0);

    const groups = new Map();
    entries.forEach((entry) => {
      if (!groups.has(entry.parent)) groups.set(entry.parent, []);
      groups.get(entry.parent).push(entry);
    });

    let bestEntries = [];
    groups.forEach((groupEntries) => {
      if (groupEntries.length > bestEntries.length) bestEntries = groupEntries;
    });

    return bestEntries.length > 1 ? bestEntries : [];
  }

  function getSelectedEntries() {
    return findEntries().filter((entry) => {
      return entry.nodes.some((node) => {
        if (node.nodeType !== Node.ELEMENT_NODE) return false;

        const checkbox = node.matches('input[type="checkbox"]')
          ? node
          : node.querySelector('input[type="checkbox"]');

        return checkbox && checkbox.checked && !isControlCheckbox(checkbox);
      });
    });
  }

  function editSelectedEntryTitle() {
    const selectedEntries = getSelectedEntries();

    if (selectedEntries.length !== 1) {
      setStatus('Select exactly one entry to rename');
      return;
    }

    const link = getEntryLink(selectedEntries[0]);
    if (!link) {
      setStatus('Selected entry link was not found');
      return;
    }

    const currentTitle = cleanText(link.textContent);
    const nextTitle = window.prompt('Edit selected watchlist entry name:', currentTitle);
    if (nextTitle === null) return;

    const cleanTitle = cleanText(nextTitle);
    if (!cleanTitle) {
      setStatus('Name was not changed');
      return;
    }

    const titles = loadCustomTitles();
    titles[normalizeHref(link.href)] = cleanTitle;
    saveCustomTitles(titles);

    link.textContent = cleanTitle;
    setStatus('Selected entry name updated');
  }

  function moveEntries(entries) {
    const parent = entries[0].parent;
    const marker = document.createComment('bt-watchlist-sort-marker');
    parent.insertBefore(marker, entries[0].nodes[0]);

    entries.forEach((entry) => {
      entry.nodes.forEach((node) => parent.insertBefore(node, marker));
    });

    parent.removeChild(marker);
  }

  function sortEntries() {
    const entries = findEntries();
    if (!entries.length) {
      setStatus('No sortable entries found');
      return;
    }

    if (!originalEntries) {
      originalEntries = entries.map((entry) => ({
        parent: entry.parent,
        title: entry.title,
        nodes: entry.nodes.slice()
      }));
    }

    const sortedEntries = entries.slice().sort((a, b) => {
      return a.title.localeCompare(b.title, undefined, {
        numeric: true,
        sensitivity: 'base'
      });
    });

    moveEntries(sortedEntries);
    setStatus(`Sorted ${sortedEntries.length} entries`);
  }

  function restoreEntries() {
    if (!originalEntries) {
      setStatus('Original order is not saved yet');
      return;
    }

    moveEntries(originalEntries);
    setStatus('Original order restored');
  }

  function setStatus(message) {
    const status = document.getElementById(STATUS_ID);
    if (status) status.textContent = message;
  }

  function insertButton() {
    if (document.getElementById(BUTTON_ID)) return;

    const entries = findEntries();

    const wrapper = document.createElement('div');
    wrapper.style.margin = '10px 0';

    const button = document.createElement('button');
    button.id = BUTTON_ID;
    button.type = 'button';
    button.textContent = 'Sort A-Z';
    button.style.marginRight = '8px';
    button.style.padding = '4px 10px';
    button.style.cursor = 'pointer';

    const editButton = document.createElement('button');
    editButton.id = EDIT_BUTTON_ID;
    editButton.type = 'button';
    editButton.textContent = 'Edit selected name';
    editButton.style.marginRight = '8px';
    editButton.style.padding = '4px 10px';
    editButton.style.cursor = 'pointer';

    const status = document.createElement('span');
    status.id = STATUS_ID;
    status.style.color = '#555';
    status.textContent = entries.length ? `Found ${entries.length} entries` : 'Sorter loaded';

    button.addEventListener('click', () => {
      if (sorted) {
        restoreEntries();
        button.textContent = 'Sort A-Z';
        sorted = false;
        return;
      }

      sortEntries();
      button.textContent = 'Restore original order';
      sorted = true;
    });

    editButton.addEventListener('click', editSelectedEntryTitle);

    wrapper.appendChild(button);
    wrapper.appendChild(editButton);
    wrapper.appendChild(status);

    const removeButton = Array.from(document.querySelectorAll('input, button')).find((element) => {
      const text = `${element.value || ''} ${element.textContent || ''}`.toLowerCase();
      return text.includes('remove checked');
    });

    if (removeButton && removeButton.parentNode) {
      removeButton.parentNode.insertBefore(wrapper, removeButton.nextSibling);
      return;
    }

    const title = Array.from(document.querySelectorAll('b, h1, h2, h3, td, div')).find((element) => {
      return cleanText(element.textContent).toLowerCase() === 'edit watchlist';
    });

    if (title && title.parentNode) {
      title.parentNode.insertBefore(wrapper, title.nextSibling);
      return;
    }

    document.body.insertBefore(wrapper, document.body.firstChild);
  }

  function init() {
    if (!isWatchlistPage()) return;
    applyCustomTitles();
    insertButton();
  }

  init();
  window.setTimeout(init, 500);
  window.setTimeout(init, 1500);
})();




Actually with time, people will have to clean their watchlists, and large watchlists will be reduce in number of topics. I agree that your userscript is useful for people who have need of sorting watched topics but I feel differently.

If I consider a thread is helpful, I will either bookmark it on my browser or add it to my documents which can be a word file or an excel file or anything document format which can help me finding such useful threads later quickly and easily.

Personally I don't have need of sorting A-Z topics in my watchlist because it's actually not too large.


Your point is valid. A-Z sorting may not be necessary for small watchlists, and it is a good habit to bookmark/document important threads.

This script is mainly for those who use watchlist as a temporary tracking tool and follow many topics at once.

A-Z sorting can be useful when searching for specific topics from a large watchlist, removing them or reviewing them.

It is not necessary for all users, but it can be a small convenience tool for heavy watchlist users.

From your feedback I got the idea for an export/bookmark style feature for a future version.
satscraper
Legendary
*
Offline

Activity: 1484
Merit: 2766



View Profile
Today at 07:33:34 PM
Last edit: Today at 07:50:54 PM by satscraper
 #5

~

Well, I have updated to the new script version. Both "Sort A–Z" and "Edit selected name" buttons are in place and functioning as they should. However, the only problem at least for me, since I prefer the browser to clear data on exit is that local storage data is also deleted when browser closes.

As a result, the changes made don’t persist, and there’s seems to be no way to add an exclusion for Bitcointalk in Brave, which I tend to use. This makes the "Edit selected name" button useless in this case because it requires some tine to rename which is painfull task when the list is big. The "Sort A–Z" button remains useful, because even though relevant data doesn’t survive after exit, it still only takes one click to reorder the full list.

The workaround could be to save the relevant key and value for local storage and add them manually after the browser is opened, but that would also take some time. I’ll probably think about this though I’m not sure yet.

For those users who dont clear browsers on exit  "Edit selected name" button remains useful.

Quote

▄▄███████████████████▄▄
▄███████████████████████▄
████████████████████████
█████████████████████████
████████████████████████
████████████▀██████▀████
████████████████████████
█████████▄▄▄▄███████████
██████████▄▄▄████████████
████████████████████████
████████████████▀▀███████
▀███████████████████████▀
▀▀███████████████████▀▀
 
 EARNBET 
██
██
██
██
██
██
██
██
██
██
██
██
██
███████▄▄███████████
████▄██████████████████
██▀▀███████████████▀▀███
▄████████████████████████
▄▄████████▀▀▀▀▀████████▄▄██
███████████████████████████
█████████▌██▀████████████
███████████████████████████
▀▀███████▄▄▄▄▄█████████▀▀██
▀█████████████████████▀██
██▄▄███████████████▄▄███
████▀██████████████████
███████▀▀███████████
██
██
██
██
██
██
██
██
██
██
██
██
██


▄▄▄
▄▄▄███████▐███▌███████▄▄▄
█████████████████████████
▀████▄▄▄███████▄▄▄████▀
█████████████████████
▐███████████████████▌
███████████████████
███████████████████
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀

 King of The Castle 
 $200,000 in prizes
██
██
██
██
██
██
██
██
██
██
██
██
██

 62.5% 

 
RAKEBACK
BONUS
Pages: [1]
  Print  
 
Jump to:  

Powered by MySQL Powered by PHP Powered by SMF 1.1.19 | SMF © 2006-2009, Simple Machines Valid XHTML 1.0! Valid CSS!