├── images
├── icon.png
├── .DS_Store
├── icon.afphoto
├── screenshot1.png
└── icon_with_padding.png
├── manifest.json
├── README.md
├── LICENSE
└── script.js
/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/komcdo/hot_search/HEAD/images/icon.png
--------------------------------------------------------------------------------
/images/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/komcdo/hot_search/HEAD/images/.DS_Store
--------------------------------------------------------------------------------
/images/icon.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/komcdo/hot_search/HEAD/images/icon.afphoto
--------------------------------------------------------------------------------
/images/screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/komcdo/hot_search/HEAD/images/screenshot1.png
--------------------------------------------------------------------------------
/images/icon_with_padding.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/komcdo/hot_search/HEAD/images/icon_with_padding.png
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "Twitter Hot Search",
4 | "version": "1.1",
5 | "content_scripts": [
6 | {
7 | "matches": [
8 | "https://*.twitter.com/*"
9 | ],
10 | "js": ["script.js"]
11 | }
12 | ]
13 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Twitter Hot Search
2 | ## Features
3 | - Supports 13 search functions
4 | - Activates proper typeahead for selecting users
5 | - Fully working TamperMonkey script
6 | - Persists across pages
7 | - Keyboard or click activation
8 | - Enhanced user protection
9 | ## Learn the code
10 | I added "LTC" (learn this code) steps in the code. Read them in order to quickly get familiar with how to code is structured. Dive into the details from there!
11 | ## Contribute!
12 | Let's keep making this better! File a bug in the "Issues" section, report it to [komcdo_](https://twitter.com/komcdo_) on Twitter, or best of all fix it yourself and submit a Pull Request!
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 komcdo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/script.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Twitter Hot Search
3 | // @namespace http://tampermonkey.net/
4 | // @version 1.1
5 | // @description Twitter Hot Search
6 | // @author komcdo
7 | // @match https://twitter.com/*
8 | // @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com
9 | // @grant none
10 | // ==/UserScript==
11 |
12 | (async function() {
13 | 'use strict';
14 | let tokenAnimationSpeed = ".3s .3s"; // speed, delay
15 | const style = document.createElement('style');
16 | style.textContent = `
17 | .hotzearch {font-family: TwitterChirp, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; transition: transform ${tokenAnimationSpeed}; will-change: transform;}
18 | .hotzearch.noDelayAnim {transition: transform 0.3s;}
19 | .hotzearch .inlineToken { position: relative; top: 8px; left: 5px; background: #5e5e5e; padding: 3px 16px 5px; border-radius: 13px; display: block; white-space: nowrap; height: 19px; display: none; max-width: 100%; transition: width ${tokenAnimationSpeed}, padding ${tokenAnimationSpeed}; will-change: width, padding;}
20 | .hotzearch .inlineToken::first-letter { text-transform:capitalize; }
21 | .hotzearch .inlineToken.invisible {opacity: 0}
22 | .hotzearch .inlineToken.minSize.creating {width: 0px !important; padding: 3px 0 5px;}
23 | .hotzearch .inlineToken.minSize.deleting {width: 0px !important; padding: 3px 0 5px;}
24 | .hotzearch .hotzearch_inline .inlineToken {display: block;}
25 | .hotzearch .staticToken { white-space: nowrap; position: relative; float: left; color: white; background: #5d5d5d; font-size: 14px; padding: 8px 16px; line-height: 10px; border-radius: 16px; cursor: pointer; margin:0 8px 8px 0; opacity: 1; transition: width ${tokenAnimationSpeed}, height ${tokenAnimationSpeed}, margin ${tokenAnimationSpeed}; will-change: width, height, margin-top;}
26 | .hotzearch .staticToken::first-letter { text-transform:capitalize; }
27 | .hotzearch .staticToken .staticTokenRemove{ height: 16px; width: 16px; position: absolute; right: 2px; top: 2px; background: #cfcfcf; border-radius: 50%; padding: 3px 0px 3px 6px; line-height: 10px; color: black; opacity: 0;}
28 | .hotzearch .staticToken:hover .staticTokenRemove {opacity: 1;}
29 | .hotzearch .staticToken.invisible {opacity: 0}
30 | .hotzearch .cloneToken {display: block; position: absolute; z-index: 1; transition: transform ${tokenAnimationSpeed}, font-size ${tokenAnimationSpeed}, padding ${tokenAnimationSpeed}, line-height ${tokenAnimationSpeed}, height ${tokenAnimationSpeed}; will-change: transform, font-size, padding, line-height, height;}
31 | .hotzearch .zearchTokenWrap {position: relative; height: 0; padding-bottom: 4px; margin-top: -4px; transition: margin-top ${tokenAnimationSpeed}, height ${tokenAnimationSpeed}; will-change: margin-top, height;}
32 | .hotzearch .zearchTokenWrap.noDelayAnim {transition: margin-top .3s, height .3s; }
33 | .hotzearch .filterButton { position: relative; top: 7px; margin-right: 5px; height: 20px; width: 20px; color: #999da1; border-radius: 13px; padding: 4px 6px; display: block; white-space: nowrap; cursor: pointer;}
34 | .hotzearch .filterDialog { position: absolute; top: 41px; right: -7px; background: #36393f; padding-right: 8px; border-radius: 16px;}
35 | .hotzearch .filterDialog select{ background: #4a4c52; border: none; font-size: 15px; border-top-left-radius: 16px; border-bottom-left-radius: 16px; padding: 5px 0px 5px 15px; color: white;}
36 | .hotzearch .filterDialog input{ background: none; border: none; padding: 6px 0 6px 6px; font-size: 15px; outline: none; width: 200px; color: white }
37 | .hotzearch.noAnim {transition: none !important;}
38 | .hotzearch.noAnim .inlineToken{transition: none;}
39 | .hotzearch.noAnim .staticToken{transition: none;}
40 | .hotzearch.noAnim .zearchTokenWrap{transition: none !important;}
41 | @media (prefers-color-scheme: light) {
42 | .hotzearch .filterDialog select,
43 | .hotzearch .staticToken,
44 | .hotzearch .cloneToken,
45 | .hotzearch .hotzearch_inline .inlineToken { background: #1d9bf0; color: white; }
46 | .hotzearch .filterDialog { background: #e6e6e6; }
47 | .hotzearch .staticToken .staticTokenRemove{ background: #fafafa; }
48 | .hotzearch .filterDialog input{ color:black;}
49 | .hotzearch .filterDialog select{ color:black;}
50 | }`;
51 | document.head.append(style);
52 | const closeBtn = ``;
53 | const filterBtn = ``;
54 |
55 | function waitForElm(selector) {
56 | return new Promise(resolve => {
57 | if (document.querySelector(selector)) return resolve(document.querySelector(selector));
58 | let elmFinder = setInterval(()=>{
59 | if (document.querySelector(selector)) {
60 | clearInterval(elmFinder);
61 | return resolve(document.querySelector(selector));
62 | }
63 | }, 50);
64 | });
65 | }
66 |
67 | const setNativeValue = (element, value) => {
68 | const valueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
69 | const prototype = Object.getPrototypeOf(element);
70 | const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value').set;
71 | if (valueSetter && valueSetter !== prototypeValueSetter) {
72 | prototypeValueSetter.call(element, value);
73 | } else {
74 | valueSetter.call(element, value);
75 | }
76 | element.dispatchEvent(new Event('input', { bubbles: true }));
77 | }
78 |
79 | const refreshInlineToken = (newInlineContent) => {
80 | if(newInlineContent) inlineContent = {...inlineContent, ...newInlineContent};
81 | if(!inlineContent.method) return removeInlineToken();
82 | inlineTokenExists = true;
83 | let text = inlineContent.method;
84 | if(inlineContent.value) {
85 | text += " " + inlineContent.value.trim().replace(/,/g, " or ").trim();
86 | }
87 | searchInput.parentElement.classList.add("hotzearch_inline");
88 | inlineToken.textContent = text;
89 | }
90 |
91 | const removeInlineToken = () => {
92 | inlineTokenExists = false;
93 | searchInput.parentElement.classList.remove("hotzearch_inline");
94 | }
95 |
96 | const createStaticToken = (method, value, addColon, preventAnim) => {
97 | let allowAnim = !preventAnim && inited;
98 | let newTokenId = (Math.random() + 1).toString(36).substring(7);
99 | let newToken = document.createElement("div");
100 | newToken.classList.add("staticToken");
101 | allowAnim && newToken.classList.add("invisible");
102 | newToken.addEventListener("click", removeStaticToken);
103 | newToken.setAttribute("data-id", newTokenId);
104 | let removeButton = "
"+closeBtn+"
";
105 | staticTokens.push({
106 | method,
107 | value,
108 | id: newTokenId,
109 | elem : newToken,
110 | status: "active"
111 | });
112 | addColon && (method += ":");
113 | newToken.innerHTML = method +" "+ value + removeButton;
114 | staticTokenWrap.append(newToken);
115 | animateStaticTokenChange(newToken, "push", allowAnim)
116 | }
117 |
118 | const removeStaticToken = (event) => {
119 | let tokenId = event.target.closest(".staticToken").getAttribute("data-id");
120 | let staticTokenObj = staticTokens.find(token => token.id === tokenId);
121 | if(staticTokenObj && staticTokenObj.elem) {
122 | if(!event.target.closest(".staticTokenRemove")){
123 | // Close button wasn't clicked, so the token body was clicked -> Edit token
124 | refreshInlineToken({method: staticTokenObj.method, value: null, spaceAfterValue: null, searchString: staticTokenObj.value.trim()});
125 | setNativeValue(searchInput, staticTokenObj.value.trim());
126 | searchInput.focus();
127 | }
128 | animateStaticTokenChange(staticTokenObj.elem, "pull", false);
129 | staticTokenObj.elem.remove();
130 | staticTokenObj.status = "removed";
131 | }
132 | }
133 |
134 | const animateStaticTokenChange = (staticToken, direction, allowAnim) => {
135 | !allowAnim && primaryColumn.classList.add("noAnim");
136 | let src, target
137 | if(direction == "push") [src, target] = [inlineToken, staticToken];
138 | if(direction == "pull") [src, target] = [staticToken, inlineToken];
139 | const wrapRect = staticTokenWrap.getBoundingClientRect();
140 | const srcRect = src.getBoundingClientRect();
141 | const targetRect = target.getBoundingClientRect();
142 | const compSrc = getComputedStyle(src);
143 | const compTarget = getComputedStyle(target);
144 | let clone = src.cloneNode(true);
145 | clone.classList.add("cloneToken");
146 | clone.style = `top: ${srcRect.top - wrapRect.top}px;
147 | left: ${srcRect.left - wrapRect.left}px;
148 | font-size: ${getComputedStyle(src).fontSize};
149 | padding: ${getComputedStyle(src).padding};
150 | line-height: ${getComputedStyle(src).lineHeight};
151 | height: ${getComputedStyle(src).height};`;
152 | allowAnim && direction == "push" && staticTokenWrap.append(clone); //Pull animation needs work
153 | src.setAttribute("style", `width: ${srcRect.width}px`);
154 | setTimeout(() => {src.classList.add("minSize", "invisible", "deleting")}, 0);
155 | target.classList.remove("minSize");
156 | clone.style = `top: ${srcRect.top - wrapRect.top}px;
157 | left: ${srcRect.left - wrapRect.left}px;
158 | transform: translate(${targetRect.left - srcRect.left}px, ${targetRect.top - srcRect.top}px);
159 | font-size: ${getComputedStyle(target).fontSize};
160 | padding: ${getComputedStyle(target).padding};
161 | line-height: ${getComputedStyle(target).lineHeight};
162 | height: ${getComputedStyle(target).height};`;
163 | if((direction == "push" && targetRect.left - wrapRect.left == 0) ||
164 | (direction == "pull" && srcRect.left - wrapRect.left == 0)){
165 | let lineWidth = staticTokenWrap.clientWidth, thisLineWidth = 0, lineCount = 0;
166 | staticTokenWrap.childNodes.forEach(token => {
167 | if(token == src || token == clone) return;
168 | if(thisLineWidth == 0 || thisLineWidth + token.clientWidth > lineWidth){
169 | lineCount++;
170 | thisLineWidth = token.clientWidth + 8;
171 | }else{
172 | thisLineWidth += token.clientWidth + 8;
173 | }
174 | })
175 | staticLineCount = lineCount;
176 | if(direction == "pull") {
177 | primaryColumn.classList.add("noDelayAnim");
178 | staticTokenWrap.classList.add("noDelayAnim");
179 | }
180 | let setTopHeight = () => {
181 | let wrapHeight = staticLineCount * 34;
182 | staticTokenWrap.style = `margin-top: ${0 - wrapHeight - 4}px; height: ${wrapHeight}px`;
183 | primaryColumn.style.transform = "translateY("+(wrapHeight + 10)+"px)";
184 | }
185 | allowAnim && setTimeout(setTopHeight, 0);
186 | !allowAnim && setTopHeight();
187 | }
188 | let animationTime = allowAnim && direction == "push" ? 600 : 0;
189 | setTimeout(() => {
190 | !allowAnim && primaryColumn.classList.remove("noAnim");
191 | target.classList.remove("invisible", "creating");
192 | clone.remove();
193 | if(direction == "pull") src.remove(); // Remove staticToken
194 | if(direction == "push"){
195 | inlineContent = {...inlineContent, method: null, value: null, spaceAfterValue: null}
196 | refreshInlineToken();
197 | }
198 | src.style.width = null;
199 | src.classList.remove("invisible", "minSize", "deleting");
200 | staticTokenWrap.classList.remove("noDelayAnim");
201 | }, animationTime);
202 | }
203 |
204 | const getInlineTokenAsText = () => {
205 | if(!inlineContent.method) return "";
206 | if(!inlineContent.value) return inlineContent.method + ":";
207 | return inlineContent.method + ":" +inlineContent.value + " ";
208 | }
209 |
210 | const methods = {
211 | spaceIsEnter: ["from","to","lang","mentions","faves","replies","retweets","min_faves","min_replies","min_retweets"],
212 | anyFillIsEnter: ["until","since","before","after"],
213 | requireEnter: ["all","any","exact","none"],
214 | }
215 | methods.all = [...methods.spaceIsEnter, ...methods.anyFillIsEnter, ...methods.requireEnter];
216 | methods.regex = methods.all.join("|");
217 |
218 | const checkForParensText = (searchString) => {
219 | // Find text filter (any, all, exact, none) in search string:
220 | let fullMatch, match, method, value;
221 | let regex = /\((.*?)\) ?/g;
222 | while ((match = regex.exec(searchString)) != null) {
223 | [fullMatch, value] = match;
224 | method = "all"; // Default unless overridden
225 | if(value.indexOf(" OR ") > -1) [method, value] = ["any", value.replace(/ OR /g, " ")]
226 | if(value.indexOf("-") > -1) [method, value] = ["none", value.replace(/-/g, "")]
227 | if(value.startsWith("@")) [method, value] = ["mentions", value.replace(/ OR /g, " ")]
228 | if(value.startsWith("\"") && value.endsWith("\"")) [method, value] = ["exact", value.replace(/\"/g, "")]
229 | inited && refreshInlineToken({method, value});
230 | createStaticToken(method, value, true);
231 | }
232 | return searchString.replace(regex, "");
233 | }
234 |
235 | const processSearchString = (origString) => {
236 | // LTC4: Regex match the search string to pull out tokens. This matches either the full search string on page load
237 | // or the input from a keydown event.
238 | // Test regex at: https://regex101.com/r/MuEJ59/1
239 | let regex = new RegExp("\\(?("+methods.regex+")(?:: ?)(.+?)?\\)?($|(? (from:u1 or u2)
249 | if(searchString || spaceAfterValue == " "){
250 | if(!value.trim().endsWith(" or") && !value.trim().endsWith(",")){
251 | // Inline token is complete, and we can set it to be static
252 | if(["before", "after", "since", "until"].includes(method)) searchInput.type = "text";
253 | if(["min_faves","min_replies","min_retweets"].includes(method)) method = method.replace("min_", "");
254 | inited && refreshInlineToken({method, value, spaceAfterValue, searchString});
255 | // LTC5: A token was found, push it to a static token
256 | createStaticToken(method, value);
257 | [method, value, spaceAfterValue, searchString] = [null, null, null, searchString || ""];
258 | }
259 | searchString = checkForParensText(searchString);
260 | }else{
261 | // Inline token not complete, set value as the editable search string
262 | let valueParts = /(.*[ ,])(.*)$|(.*)/g.exec(value);
263 | value = valueParts[1];
264 | searchString = valueParts[2] || valueParts[3];
265 | }
266 | }
267 | }
268 | // LTC6: Methods like any, all, exact and none don't have a method prefix. Find these matches with separate regex.
269 | searchString = checkForParensText(searchString || "");
270 | inlineContent = {method, value, spaceAfterValue, searchString};
271 | if(method){
272 | // LTC7: After all full token matches, a "method" still remains so show the inline token
273 | refreshInlineToken();
274 | if(["from", "to", "mentions"].includes(method) && searchString[0] != "@")
275 | searchString = "@" + searchString;
276 | }
277 | // LTC8: After all static and inline tokens, set any remaining text to be editable.
278 | setNativeValue(searchInput, searchString);
279 | }
280 |
281 | const protectUser = (cont) => {
282 | let protectionContent = document.createElement("div");
283 | protectionContent.innerHTML = 'Search blocked
To make Twitter more fun, our AI has detected this account is not funny and has blocked searching for this user\'s tweets.
'
284 | cont.append(protectionContent);
285 | return true;
286 | }
287 |
288 | const userSelectedFromList = () => {
289 | const typeaheadSelected = searchForm.querySelector("[data-testid='typeaheadResult'][aria-selected=true]");
290 | if(!typeaheadSelected) return false;
291 | const username = typeaheadSelected.querySelectorAll("span")[2].textContent.slice(1);
292 | if(username == "kathygriffin") return protectUser(typeaheadSelected);
293 | processSearchString("@" + username + " ");
294 | return true;
295 | }
296 |
297 | let selectOptions = "";
298 | methods.all.filter(x => !["after", "before", "until", "since", "min_faves", "min_replies", "min_retweets"].includes(x)).forEach(method => {
299 | selectOptions+=``;
300 | })
301 | const filterDialog = `
302 |
303 |
304 |
`;
305 | let filterDialogShown = false;
306 | const showFilterDialog = () => {
307 | event.preventDefault();
308 | event.stopPropagation();
309 | if(!filterDialogShown){
310 | filterDialogShown = true;
311 | filterButton.innerHTML = filterBtn + filterDialog; // Can't hide/show with a class because on search submit Twitter will select this input
312 | filterButton.querySelector("input").addEventListener("keydown", e => e.key == "Enter" && submitFilterDialog());
313 | }
314 | }
315 | const hideFilterDialog = () => {
316 | filterDialogShown = false;
317 | filterButton.innerHTML = filterBtn;
318 | }
319 | const submitFilterDialog = () => {
320 | let [select, input] = [filterButton.querySelector("select"), filterButton.querySelector("input")];
321 | let includeColon = methods.requireEnter.includes(select.value) ? true : false;
322 | createStaticToken(select.value, input.value, includeColon, true);
323 | hideFilterDialog();
324 | select.value = "from";
325 | input.value = "";
326 | setTimeout(()=>searchInput.focus(), 0);
327 | }
328 |
329 | const tokenizeSearch = (event) => {
330 | // LTC2: On key down, evaluate the entry
331 | if(event.key == "ArrowUp" || event.key == "ArrowDown") return true; // Up/Down arrows
332 |
333 | let value = event.target.value.trimStart();
334 |
335 | if(event.key == "Backspace" && event.target.value == ""){
336 | if(inlineContent.value){
337 | setNativeValue(searchInput, inlineContent.value);
338 | inlineContent.value = null
339 | }else{
340 | inlineContent.method = null;
341 | }
342 | refreshInlineToken();
343 | return;
344 | }
345 | if(event.key == "Enter" && (inlineTokenExists || staticTokens.length)){
346 | if(userSelectedFromList()){
347 | // Enter was pressed to select a user from the typeahead list
348 | event.stopPropagation()
349 | event.preventDefault();
350 | return false;
351 | }
352 | if(methods.requireEnter.includes(inlineContent.method)){
353 | refreshInlineToken({...inlineContent, value: inlineContent.searchString, spaceAfterValue: "", searchString: ""});
354 | createStaticToken(inlineContent.method, inlineContent.value, true);
355 | setNativeValue(searchInput, "");
356 | return false;
357 | }
358 | // Reconstruct full query, and submit
359 | let prepend = "";
360 | staticTokens.forEach((token) => {
361 | if(token.status == "active") {
362 | let {method, value} = token;
363 | if(method == "any") [method, value] = [null, value.replace(/ +/g, " OR ")];
364 | if(method == "all") [method, value] = [null, value];
365 | if(method == "exact") [method, value] = [null, "\"" + value + "\""];
366 | if(method == "none") [method, value] = [null, value.replace(/(^| +)/g, "$1-")];
367 | if(method == "mentions") [method, value] = [null, value.replace(/ *, *| +or +/g, " OR ")];
368 | if(["faves", "replies", "retweets"].includes(method)) method = "min_"+method;
369 | if(["from", "to", "mentions"].includes(method)) value = value.replace(/ *, *| +or +/g, " OR " + token.method +":");
370 | method = method ? method + ":" : "";
371 | prepend = prepend + "("+method+value+") ";
372 | }
373 | });
374 | prepend += getInlineTokenAsText();
375 | setNativeValue(searchInput, prepend + value);
376 | return true;
377 | }
378 | if(event.key == " " && value.endsWith("@")) return false;
379 | if (event.key.length == 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
380 | // LTC3: Add key entry to value
381 | event.stopPropagation();
382 | event.preventDefault();
383 | if(searchInput.selectionStart != value.length) value = value.trimLeft(); // Remove trailing spaces that were not just entered
384 | let newInput = event.key;
385 | if(event.key == "," && value.startsWith("@")) newInput = " or @";
386 | value = value.slice(0, searchInput.selectionStart) + newInput + value.slice(searchInput.selectionEnd);
387 | processSearchString(value);
388 | searchInput.type != "date" && (searchInput.selectionStart += newInput.length);
389 | }
390 | };
391 |
392 | let inited, searchInput, searchForm, primaryColumn, inlineToken, inlineContent, staticTokens, staticTokenWrap, staticLineCount, inlineTokenExists, filterButton;
393 | const init = async () => {
394 | // LTC1: Find and create page elements, add event listers
395 | inited = false;
396 | searchInput = await waitForElm("[data-testid='SearchBox_Search_Input']");
397 | searchInput.placeholder = "Twitter Hot Search";
398 | searchForm = searchInput.closest("form");
399 | primaryColumn = searchForm.closest("[data-testid='primaryColumn'] > div") || searchForm.closest("[data-testid='sidebarColumn'] > div") ;
400 | primaryColumn.classList.add("hotzearch", "initing", "noAnim");
401 | if(inlineToken && inlineToken.isConnected) inlineToken.remove();
402 | inlineToken = document.createElement("div")
403 | inlineToken.classList.add("inlineToken");
404 | searchInput.parentElement.prepend(inlineToken);
405 | if(staticTokenWrap && staticTokenWrap.isConnected) staticTokenWrap.remove();
406 | staticTokenWrap = document.createElement("div")
407 | staticTokenWrap.classList.add("zearchTokenWrap");
408 | searchForm.parentElement.prepend(staticTokenWrap);
409 | searchForm.querySelector('* > div').style.zIndex = 1;
410 | staticTokenWrap.innerHTML = "";
411 | staticTokens = [];
412 | staticLineCount = 0;
413 | inlineContent = {};
414 | inlineTokenExists = false;
415 | if(filterButton && filterButton.isConnected) filterButton.remove();
416 | filterDialogShown = false;
417 | filterButton = document.createElement("div");
418 | filterButton.classList.add("filterButton");
419 | filterButton.innerHTML = filterBtn;
420 | searchInput.parentElement.append(filterButton);
421 |
422 | filterButton.addEventListener("click", showFilterDialog);
423 | document.addEventListener("click", hideFilterDialog);
424 | searchInput.addEventListener("keydown", tokenizeSearch);
425 | let lastValue = "";
426 | searchInput.addEventListener("input", (e) => {
427 | if(lastValue == searchInput.value) return true;
428 | lastValue = searchInput.value;
429 | if(["before", "after", "since", "until"].includes(inlineContent.method)){
430 | if(searchInput.type == "date" && searchInput.value.indexOf('m') == -1){
431 | processSearchString(searchInput.value + " ");
432 | searchInput.type = "text";
433 | lastValue = "";
434 | }else{
435 | searchInput.type = "date";
436 | searchInput.showPicker();
437 | }
438 | }else{
439 | searchInput.type = "text";
440 | }
441 | });
442 | searchForm.addEventListener("click", (event) => {
443 | const el = event.target.closest("[data-testid='typeaheadResult']");
444 | if(el){
445 | el.setAttribute("aria-selected", true);
446 | if(userSelectedFromList(el)) {
447 | return event.stopPropagation() && event.preventDefault() && false;
448 | };
449 | }
450 | })
451 | searchInput.addEventListener('DOMNodeRemoved', (e) => e.target == searchInput && init());
452 |
453 | // LTC9: On page load, tokenize the query
454 | processSearchString(searchInput.value);// Evalute searchInput.value on page load
455 | primaryColumn.classList.remove("initing", "noAnim");
456 | inited = true;
457 | }
458 |
459 | await init();
460 |
461 | let previousLoc = {...location};
462 | let observer = new MutationObserver(function(mutations) {
463 | if (location.href !== previousLoc.href) {
464 | previousLoc = {...location};
465 | init();
466 | }
467 | });
468 | const config = {subtree: true, childList: true};
469 | observer.observe(document, config);
470 |
471 | console.log("Twitter Hot Search enabled");
472 | })();
--------------------------------------------------------------------------------