Skip to content

Commit e264b1d

Browse files
feat: infinite loading in slur hover metadata (#689)
* feat: infinite loading in metadata WIP * fix: optimize slur metadata for infinite scrolling, fix feature working on Reddit and others * fix: set slur z-index to 3 * fix: adjust metadata box position dynamically * chore: delete comments --------- Co-authored-by: Aatman Vaidya <aatmanvaidya@gmail.com>
1 parent bf74a64 commit e264b1d

File tree

1 file changed

+120
-58
lines changed

1 file changed

+120
-58
lines changed

browser-extension/plugin/src/transform-general.js

+120-58
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,29 @@ const processNewlyAddedNodesGeneral = function (firstBody) {
7575
};
7676

7777
const processNewlyAddedNodesGeneral2 = function (firstBody, jsonData) {
78-
let targetWords = jsonData.map(slur => Object.keys(slur)[0]);
78+
let targetWords = jsonData.map((slur) => Object.keys(slur)[0]);
7979
targetWords.sort((a, b) => b.length - a.length);
8080

8181
let uliStore = [];
8282
getAllTextNodes(firstBody, uliStore);
83-
locateSlur(uliStore, targetWords);
84-
addMetaData(targetWords, jsonData);
83+
84+
locateSlur(uliStore, targetWords, jsonData);
85+
86+
const ob = new MutationObserver(async (mutations) => {
87+
setTimeout(() => {
88+
mutations.forEach((mutation) => {
89+
if (mutation.type === 'childList' && mutation.addedNodes) {
90+
mutation.addedNodes.forEach((node) => {
91+
let uliStore = [];
92+
getAllTextNodes(node, uliStore);
93+
locateSlur(uliStore, targetWords, jsonData);
94+
});
95+
}
96+
});
97+
}, 10);
98+
});
99+
100+
ob.observe(firstBody, { childList: true, subtree: true });
85101
};
86102

87103
function checkFalseTextNode(text, actualLengthOfText) {
@@ -122,87 +138,133 @@ function findPositions(word, text) {
122138
return positions;
123139
}
124140

125-
function locateSlur(uliStore, targetWords) {
126-
uliStore.forEach(store => {
141+
function locateSlur(uliStore, targetWords, jsonData) {
142+
if (uliStore.length === 0) return;
143+
uliStore.forEach((store) => {
127144
let textnode = store.node;
145+
146+
if (
147+
store.parent?.classList.contains('slur') ||
148+
store.parent?.innerHTML.includes(`class="slur`)
149+
) {
150+
return;
151+
}
128152
let tempParent = document.createElement('span');
129153
tempParent.textContent = textnode.textContent;
130154
let slurPresentInTempParent = false;
131-
targetWords.forEach(targetWord => {
155+
let foundTargettedWord = '';
156+
targetWords.forEach((targetWord) => {
132157
const sanitizedTargetWord = targetWord.replace(/\s+/g, '-');
133158
const slurClass = `slur-container-${sanitizedTargetWord}`;
134-
135-
const escapedTargetWord = targetWord.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
159+
160+
const escapedTargetWord = targetWord.replace(
161+
/[.*+?^${}()|[\]\\]/g,
162+
'\\$&'
163+
);
136164
// regex for multi-word and single-word phrases
137165
const regex = new RegExp(`(^|[\\s.,!?()'"\\[\\]{}<>;:@#\\$%\\^&*+=~_“”‘’])(${escapedTargetWord})(?=[\\s.,!?()'"\\[\\]{}<>;:@#\\$%\\^&*+=~_“”‘’]|$)`, 'giu');
138-
166+
139167
if (regex.test(tempParent.textContent)) {
140-
tempParent.innerHTML = tempParent.innerHTML.replace(regex, (match, prefix, word) => {
141-
return `${prefix}<span class="${slurClass}"><span class="slur">${word}</span></span>`;
142-
});
168+
tempParent.innerHTML = tempParent.innerHTML.replace(
169+
regex,
170+
(match, prefix, word) => {
171+
return `${prefix}<span class="${slurClass}"><span class="slur">${word}</span></span>`;
172+
}
173+
);
143174
slurPresentInTempParent = true;
175+
foundTargettedWord = targetWord;
144176
}
145177
});
146178

147179
if (slurPresentInTempParent) {
148180
textnode.replaceWith(tempParent);
181+
182+
addNodeMetaData(tempParent, foundTargettedWord, jsonData);
149183
}
150184
});
151185
}
152186

153-
function addMetaData(targetWords, jsonData) {
154-
targetWords.forEach((targetWord) => {
155-
const sanitizedTargetWord = targetWord.replace(/\s+/g, '-');
156-
const className = `slur-container-${sanitizedTargetWord}`;
157-
const elements = Array.from(document.querySelectorAll(`.${className}`));
158-
159-
elements.forEach((element) => {
160-
const slur = element.querySelector('.slur');
161-
if (!slur) return;
162-
163-
// Add hover styles
164-
slur.style.backgroundColor = '#ffde2155';
165-
slur.style.boxShadow = '0px 0px 5px #ffde21';
166-
slur.style.cursor = 'pointer';
167-
168-
// Create a single tooltip container and React root if not already created
169-
let tooltipContainer = document.getElementById('slur-tooltip-container');
170-
if (!tooltipContainer) {
171-
tooltipContainer = document.createElement('div');
172-
tooltipContainer.id = 'slur-tooltip-container';
173-
tooltipContainer.style.position = 'absolute';
174-
tooltipContainer.style.zIndex = '1000000000';
175-
document.body.appendChild(tooltipContainer);
176-
tooltipContainer.root = createRoot(tooltipContainer);
187+
function addNodeMetaData(element, targetWord, jsonData) {
188+
const slur = element.querySelector('.slur');
189+
if (!slur) return;
190+
191+
slur.style.backgroundColor = '#ffde2155';
192+
slur.style.boxShadow = '0px 0px 5px #ffde21';
193+
slur.style.cursor = 'pointer';
194+
slur.style.zIndex = '3';
195+
slur.style.position = 'relative';
196+
slur.style.pointerEvents = 'auto';
197+
198+
let tooltipContainer = document.getElementById('slur-tooltip-container');
199+
if (!tooltipContainer) {
200+
tooltipContainer = document.createElement('div');
201+
tooltipContainer.id = 'slur-tooltip-container';
202+
tooltipContainer.style.position = 'absolute';
203+
tooltipContainer.style.zIndex = '999';
204+
document.body.appendChild(tooltipContainer);
205+
tooltipContainer.root = createRoot(tooltipContainer);
206+
}
207+
208+
// Find the slur details from jsonData
209+
const matchedSlur = jsonData.find(
210+
(slur) =>
211+
Object.keys(slur)[0].toLowerCase() === targetWord.toLowerCase()
212+
);
213+
const slurDetails = matchedSlur
214+
? matchedSlur[Object.keys(matchedSlur)[0]]
215+
: {};
216+
217+
const handleMouseOver = () => {
218+
const rect = slur.getBoundingClientRect();
219+
const screenHeight = window.innerHeight;
220+
const screenWidth = window.innerWidth;
221+
222+
// Initial position (assume tooltip goes below first)
223+
let position = {
224+
x: rect.left + window.scrollX,
225+
y: rect.bottom + 10 + window.scrollY
226+
};
227+
228+
// Render tooltip first (off-screen)
229+
tooltipContainer.style.top = `-9999px`; // Prevent flickering
230+
tooltipContainer.style.left = `-9999px`;
231+
tooltipContainer.style.opacity = '0';
232+
233+
tooltipContainer.root.render(
234+
<HoverSlurMetadata slurDetails={slurDetails} />
235+
);
236+
237+
// Wait for the next render cycle to measure the height
238+
setTimeout(() => {
239+
const tooltipHeight = tooltipContainer.offsetHeight;
240+
241+
const spaceBelow = screenHeight - rect.bottom;
242+
243+
if (spaceBelow < tooltipHeight + 10) {
244+
position.y = rect.top - tooltipHeight - 10 + window.scrollY;
177245
}
178246

179-
// Find the slur details from jsonData
180-
const matchedSlur = jsonData.find(slur =>
181-
Object.keys(slur)[0].toLowerCase() === targetWord.toLowerCase()
182-
);
183-
const slurDetails = matchedSlur ? matchedSlur[Object.keys(matchedSlur)[0]] : {};
247+
if (position.x + 290 > screenWidth) {
248+
position.x = screenWidth - 320 + window.scrollX;
249+
}
184250

185-
const handleMouseOver = () => {
186-
const rect = slur.getBoundingClientRect();
187-
tooltipContainer.style.top = `${rect.bottom + window.scrollY}px`;
188-
tooltipContainer.style.left = `${rect.left + window.scrollX}px`;
251+
tooltipContainer.style.top = `${position.y}px`;
252+
tooltipContainer.style.left = `${position.x}px`;
253+
tooltipContainer.style.opacity = '1';
254+
}, 0); // Short delay to ensure the DOM updates
255+
};
189256

190-
tooltipContainer.root.render(
191-
<HoverSlurMetadata slurDetails={slurDetails} />
192-
);
193-
};
257+
const handleMouseOut = () => {
258+
tooltipContainer.root.render(null);
259+
};
194260

195-
const handleMouseOut = () => {
196-
tooltipContainer.root.render(null);
197-
};
261+
slur.addEventListener('mouseover', handleMouseOver);
262+
slur.addEventListener('mouseout', handleMouseOut);
198263

199-
slur.addEventListener('mouseover', handleMouseOver);
200-
slur.addEventListener('mouseout', handleMouseOut);
201-
});
202-
});
264+
element.classList.add('slur-metadata-added');
203265
}
204266

205267
export default {
206268
processNewlyAddedNodesGeneral,
207-
processNewlyAddedNodesGeneral2,
208-
};
269+
processNewlyAddedNodesGeneral2
270+
};

0 commit comments

Comments
 (0)