Detect page find/search to expand a collapsible - javascript

My team is using an expandable box to condense info on our wiki (in Confluence), it's pretty standard using display:none/block. I'd like this to work with the browser's find functionality. I've found that when I switch to hiding the content using max-height the browser at least finds the text correctly, but I'd like to expand the collapsible when a match is found inside it and re-collapse it when find is no longer looking at it. Is there a way to do that?
I've already tried the focus and selectionchange events to no avail. I guess I could track scrolling for jumps or track keystrokes but neither of those really tells me if the collapsible is where the query was found.
tl;dr is there a way to detect browser find?
Update: Here's an idea of what the code looks like:
var expand = document.querySelector('.expand');
expand.querySelector('.head').addEventListener('click', function(e) {
//toggle a class .show on .expand
})
And my CSS:
.expand .body {
opacity: 0;
max-height: 0;
padding: 0 20px 0 40px;
transition: all .4s;
}
.expand.show .body {
max-height: 3000px;
opacity: 1;
padding: 10px 20px 20px 40px;
}

Actually you can detect key press combination such as Ctrl+F like below
var map = {17: false, 70: false};
$(document).keydown(function(e) {
if (e.keyCode in map) {
map[e.keyCode] = true;
if (map[17] && map[70]){
// FIRE EVENT YOU EVENT
}
}
}).keyup(function(e) {
if (e.keyCode in map) {
map[e.keyCode] = false;
}
});

Related

How to make this div scrollTop value match that of the textarea?

I'm using a div to format and display the text from a textarea of equal dimensions and I need them to be permanently in sync. However, I haven't been able to synchronize their respective scrollTops after the input text goes past the bottom of the textarea.
My process has been similar to the one described here, however I can't get his solution to work on my project.
Here's a demo and snippets of the minimum relevant code:
<section>
<div class="input-text__container">
<div id="input-text--mirror" class="input-text"></div>
<textarea
id="input-text--original"
cols="30"
rows="6"
autofocus
class="input-text"
placeholder="Enter your text here"
autocomplete="off"
autocorrect="off"
spellcheck="false"
></textarea>
</div>
<section>
#import url('https://fonts.googleapis.com/css2?family=Inter:wght#400;500&display=swap');
html {
font-size: 62.5%;
box-sizing: border-box;
scroll-behavior: smooth;
}
*,
*::after,
*::before {
margin: 0;
padding: 0;
border: 0;
box-sizing: inherit;
vertical-align: baseline;
}
body {
height: 100vh;
}
section {
display: flex;
flex-direction: column;
min-height: 100%;
padding: 1rem;
}
.input-text__container {
width: 100%;
position: relative;
flex: 1;
}
.input-text {
width: 100%;
height: 100%;
position: absolute;
font-size: 3.2rem;
overflow-wrap: break-word;
font-family: "Inter";
}
#input-text--mirror {
background-color: #e9ecf8;
color: #0a3871;
overflow: hidden;
}
#input-text--original {
background-color: transparent;
-webkit-text-fill-color: transparent;
resize: none;
outline: none;
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
#input-text--original::placeholder {
color: #e9ecf8;
-webkit-text-fill-color: #052051;
}
#input-text--original::selection {
-webkit-text-fill-color: #ffffff;
}
.invalid {
font-weight: 400;
color: #ff0000;
}
#input-text--original::-webkit-scrollbar {
display: none;
}
let invalidInput = false;
const patterns = {
valid: "a-z ",
invalid: "[^a-z ]",
linebreaks: "\r|\r\n|\n",
};
const textIn = document.querySelector("#input-text--original");
const mirror = document.querySelector("#input-text--mirror");
function validateInput(string, className) {
let anyInvalidChar = false;
// Generate regular expressions for validation
const regExpInvalids = new RegExp(patterns.invalid, "g");
const regExpLinebreaks = new RegExp(patterns.linebreaks);
// Generate innerHTML for mirror
const mirrorContent = string.replace(regExpInvalids, (match) => {
if (regExpLinebreaks.test(match)) {
return "<br/>";
} else {
anyInvalidChar = true;
return `<span class=${className}>${match}</span>`;
}
});
// Update mirror
mirror.innerHTML = mirrorContent;
return anyInvalidChar;
}
textIn.addEventListener("input", (e) => {
const plain = textIn.value;
const newInputValidity = validateInput(plain, "invalid");
mirror.scrollTop = textIn.scrollTop;
});
textIn.addEventListener(
"scroll",
() => {
mirror.scrollTop = textIn.scrollTop;
},
{ passive: true }
);
On a desktop screen typing the first 8 natural numbers in a column should be enough to reproduce the issue.
The last thing I checked, but perhaps the most relevant so far was this. It seems to deal with the exact same issue on React, but I'm afraid I don't know how to adapt that solution to Vanilla JavaScript, since I'm just starting to learn React. Please, notice, I'm trying to find a solution that doesn't depend on libraries like jQuery or React.
Besides that, I tried the solution described in the aforementioned blog, by replacing return "<br/>"; with return "<br/> "; in my validateInput function but that didn't work. I also added a conditional to append a space to plain in const plain = textIn.value; in case the last char was a linebreak, but I had no luck.
I also included console.log commands before and after mirror.scrollTop = textIn.scrollTop; in the textIn scroll handler to track the values of each scrollTop and even when they were different, the mirror scrollTop wasn't updated. I read it might be because divs weren't scrollable by default, but adding "overflow: scroll" to its styles didn't fix the problem either.
I read about other properties related to scrollTop, like offsetTop and pageYOffset, but they're either read-only or not defined for divs.
I've reviewed the following posts/sites, too, but I've still haven't been able to fix this problem.
https://codepen.io/Goweb/pen/rgrjWx
https://stackoverflow.com/questions/68092068/making-two-textareas-horizontally-scroll-in-sync
Scrolling 2 different elements in same time
React : setting scrollTop property of div doesn't work
sync scrolling of textarea input and highlighting container
.scrollTop(0) not working for getting a div to scroll to the top
How to attach a scroll event to a text input?
I no longer remember what else I've reviewed, but nothing has worked and I no longer know what else to do. Thank you for your attention and help.
After trying to replicate the solution for a React app that I mentioned in the post, using vanilla JavaScript (demo here), I tried to apply that to my own project and all I had to do was adding a <br> tag to the mirror in the end of my validateInput function. That is: mirror.innerHTML = mirrorContent + "<br>";.
Besides that, updating the mirror's scrollTop every time the input event on the textarea was triggered was not needed. Neither was it to pass the { passive: true } argument to the scroll event.
The modified code is here:
function validateInput(string, className) {
let anyInvalidChar = false;
// Generate regular expressions for validation
const regExpInvalids = new RegExp(patterns.invalid, "g");
const regExpLinebreaks = new RegExp(patterns.linebreaks);
// Generate innerHTML for mirror
const mirrorContent = string.replace(regExpInvalids, (match) => {
if (regExpLinebreaks.test(match)) {
return "<br/>";
} else {
anyInvalidChar = true;
return `<span class=${className}>${match}</span>`;
}
});
// Update mirror
mirror.innerHTML = mirrorContent + "<br>";
return anyInvalidChar;
}
textIn.addEventListener("input", (e) => {
const plain = textIn.value;
const newInputValidity = validateInput(plain, "invalid");
});
textIn.addEventListener("scroll", () => mirror.scrollTop = textIn.scrollTop);

Optimizing jquery hide() and show() on Safari

I have a site with a large outline, and I'm trying to let our users filter it down so they can see just the stuff they want. Each line of the outline has a set of classes that say what category it's in, and I'm hide/showing them via jQuery when the users select a particular category.
Here's the current location so you can see it in action:
https://courses.edx.org/courses/course-v1:HarvardX+CHEM160+1T2017/76695c0ad7604bb897570ecb906db6e3/
And here's the javascript and css for this page:
$(document).ready(function() {
console.log('working');
// Keeping track of all the currently visible items.
var currentlyShown = [];
var index;
var showAllButton = $('#showAll');
// If any of the object's classes match any of the selected options, show it.
function showRightClasses() {
console.log('showing: ' + currentlyShown);
if (currentlyShown.length == 0) {
showAllButton.click();
}
$('.hiddenpage').each(function(i) {
if (_.intersection(this.className.split(' '), currentlyShown).length > 0) {
$(this).show('slow');
} else {
$(this).hide('slow');
}
});
}
if (showAllButton.prop('checked')) {
currentlyShown.push('hiddenpage');
showRightClasses();
}
showAllButton.change(function() {
if (!this.checked) {
index = currentlyShown.indexOf('hiddenpage');
if (index !== -1) {
currentlyShown.splice(index, 1);
}
} else {
currentlyShown.push('hiddenpage');
}
showRightClasses();
});
$('.pageselector').change(function() {
subject = $(this).attr('name');
if (!this.checked) {
index = currentlyShown.indexOf(subject);
if (index !== -1) {
currentlyShown.splice(index, 1);
}
} else {
currentlyShown.push(subject);
}
if (showAllButton.prop('checked')) {
showAllButton.click();
}
showRightClasses();
});
});
.hiddenpage {
display: none;
}
.checkboxes {
float: right;
padding: 8px;
border: 4px outset #aaa;
border-radius: 8px;
background-color: #eee;
}
.checkboxes label {
display: inline;
}
.nav-section {
font-size: 120%;
font-weight: bold;
margin-top: 1em;
}
.nav-sub {
font-weight: bold;
margin-left: 1em;
}
.nav-unit {
font-weight: normal;
margin-left: 2em;
}
This works, but on Safari it's dreadfully slow, and it's not particularly fast on Firefox either. Is there a more efficient way to hide/show the rows in this outline without losing the animation? Am I accidentally doing something foolish like having every row run code that hides every other row?
I should note that I have no ability to control the rest of the environment. I can't change the version of jQuery that the site uses, or remove Underscore, for example. I can only control the code you see above, and the HTML for the list.
First of all, if you care about speed, ditch the 'slow' param in .show('slow') and .hide('slow'). This triggers a very performance-heavy jQuery animation.
With all the frames you're loosing right now, this will not work nice anyway. If you need animation there, maybe you could try something with opacity instead, since (css-based) opacity animation is very cheap.
EDIT: just checked this on the site you linked and it works nice and snappy with just .show() and .hide(). The 'slow' param is definitely your bottleneck, so either just remove it or look for a different way to animate, if you absolutely need to.

JQuery or Javascript that will make an element fade away automatically

So I am making a clicker game and am kind of stuck. I want a popup like cookieClicker has when you get an achievement. It pops up and tells you what happened, you can click the x or it will just fade away after a few seconds.
I tried making something with pure javascript and CSS to no avail, it would fade away nicely but not automatically.
So how do I make it so whenever X element is made/displayed then it goes away after 3 seconds?
Also, if it matters the element would be created by a javascript function, and multiples might be created at the same time.
P.S. I tried searching and found something about auto-fading in javascript but nothing in there seemed to work either.
EDIT: After trying to view cookieclicker source and playing the game again it appears it doesn't even have this functionality. The closest thing I can compare it to is when you would add something to your cart on a website, then it alerts you the item was added and then fades away.
Here is one approach which uses Javascript to trigger a CSS transition:
var button = document.getElementsByTagName('button')[0];
var div = document.getElementsByTagName('div')[0];
function autoFader() {
if (window.getComputedStyle(div).getPropertyValue('display') === 'none') {
div.style.display = 'block';
setTimeout(function(){
div.style.opacity = '0';
},10);
setTimeout(function(){
div.removeAttribute('style');
},4010);
}
}
button.addEventListener('click',autoFader,false);
div {
display: none;
width: 200px;
height: 100px;
margin: 20px auto;
padding: 6px;
font-size: 20px;
color: rgb(255,255,255);
text-align: center;
border: 3px solid rgb(127,0,0);
background-color: rgb(255,0,0);
opacity: 1;
transition: opacity 3s linear 1s;
}
<button type="button">Click Me</button>
<div>
<p>Hi, I'm an auto-fading pop-up.</p>
</div>
So, your openPopup function might look like this:
function openPopup(/* options here */) {
const popup = actuallyOpenPopup();
waitSomeTimeAndCloseIfNotClosedYet(popup);
}
where 2nd function should take a popup instance (which has .close method probably, or dismiss)
and start a timeout. You need to keep that timeout, so if close was called, you need to cancel it.
Something like this:
function waitSomeTimeAndCloseIfNotClosedYet(popup) {
const originalClose = popup.close;
/* monkey patching, decorating,
separate method - whatever you prefer */
popup.close = function () {
clearTimeout(this.timeout);
originalClose.call(this);
};
popup.timeout = setTimeout(() => popup.close(), 3000);
}
So, if was closed manually - it wont be called twice, if not, will fire up a timeout and close automatically.
Via CSS you can only achieve visible closing, but not removal of nodes. (Google for transition visibility, fade out modal etc.)
Hope this helps!

Toggle class of sticky menu on scroll with overflow hidden on page

I want to add a class .custom-menu-bg to sticky menu .custom-menu on scroll, while having overflow: hidden on body. Here's my code :
<script type="text/javascript" src="css/jquery-1.7.2.min.js"></script>
<script type="text/javascript">
var _rys = jQuery.noConflict();
_rys("document").ready(function() {
_rys(window).scroll(function() {
if (_rys(this).scrollTop() > 1) {
_rys('.custom-menu').addClass("custom-menu-bg");
} else {
_rys('.custom-menu').removeClass("custom-menu-bg");
}
});
});
</script>
But this code doesn't work with overflow: hidden on body tag
so I tried :
$('html').on('DOMMouseScroll', function(e) {
var delta = e.originalEvent.detail;
if (delta < 0) {
if ($('body').hasClass('section-element-1'))
$('.custom-menu').addClass("custom-menu-bg");
} else if (delta > 0) {
$('.custom-menu').removeClass("custom-menu-bg");
}
});
But this code only works for Mozilla and it's not a solution even, it's just a temp fix or work-around.
What I want is when I scroll down $('.custom-menu').addClass("custom-menu-bg"); i.e. custom-menu-bg class gets added to custom-menu.
And when I scroll up to the top $('.custom-menu').removeClass("custom-menu-bg"); i.e. custom-menu-bg class gets removed from custom-menu.
The top of body,document,window etcetera is always 0.
And top of my div with class custom-menu also has top: 0 always.
I'm looking for a permanent solution which works on all browsers.
I've reproduced the same effect you wanted HERE.
The only change that I've brought in comparison to your code is that I've made a makeshift body div and applied overflow: hidden on it.
Then, using jQuery, you'll be checking for the scroll event triggered by a wrapper inside the body div - which is in charge of holding the content) - and not by itself (or even document).
$('.wrapper').scroll(function () {
if ($(this).scrollTop() > 0) {
$('.custom-menu').addClass("custom-menu-bg");
} else {
$('.custom-menu').removeClass("custom-menu-bg");
}
});
This is because the makeshift body div has an overflow property set to hidden, and therefore won't generate that particular scroll event (maybe it would if you had the handler registered using browser-specific scroll events). Whereas the inner wrapper div will always have it's height property determined by it's content and is therefore scrollable.
NOTE: jQuery's scroll() is cross-browser, and hence a permanent solution.
You can bind on any id or on class also . its on you for now demo i
am using window .
This single event works for both if you have scroll or not. i.e overflow:hidden or overflow:scroll
$(window).bind('mousewheel DOMMouseScroll', function(event){
if (event.originalEvent.wheelDelta > 0 || event.originalEvent.detail < 0) {
// scroll up
$('.custom-menu').removeClass("custom-menu-bg");
}
else {
// scroll down
$('.custom-menu').addClass("custom-menu-bg");
}
});
.custom-menu {
background-color: black;
height: 100px;
width: 100%
}
.custom-menu-bg{
background-color: green;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="custom-menu">
</div>
Or you can also use this jQuery mousewheel plugin https://github.com/brandonaaron/jquery-mousewheel.
//toggled is class when mobile menu is opened
let moveScroll = '';
window.onscroll = function (e) {
const navBar = document.getElementById('id-of-your-navigation-bar');
if (moveScroll > 0 && navBar.classList.contains('toggled')) {
navBar.classList.remove('toggled');
moveScroll = 0;
} else if (navBar.classList.contains('toggled')) {
moveScroll = 1;
}
};

How do I move the DOM elements down when I insert a new one so they don't overlap?

So I have a javascript function that inserts a span on clicking a button. But the problem is when I insert it, it overlaps the other elements. How can I move the other elements down when the button is clicked to make room for the inserted element? And then move them back up when the element is removed? Here is my code:
$(".top-button").on("click", function() {
if (this.nextElementSibling) {
$(this.nextElementSibling).slideToggle(600);
};
});
$(".bottom-button").on("click", function() {
$(this.nextElementSibling).slideToggle(600);
});
And a working demo here http://codepen.io/andrewcockerham/pen/xjgkL/
Basically, when I click on the yellow and green 'buttons' on Entry 1, the MP and IP boxes toggle, but they overlap the other elements (When Entry 1 is collapsed, {click on it}). How can I make the other elements move out to make room when the MP and IP appear, then return to their normal place when the MP or IP disappear?
I've tried appendChild(), insertAfter(), insertBefore(), all without success.
Please forgive the ugly demo and ugly code - its a WIP! Thanks!
So I figured out how to do it, so I'll answer my own question.
Basically I just inserted a blank or empty div 'behind' the inserted span, thus moving the DOM down or up. Here's the JS code:
$(".top-button").on("click", function() {
if (this.nextElementSibling) {
if ($(this.nextElementSibling).css('display') == "none") {
// insert blank element to move the DOM down
$("<div class='top-blank'><span></span></div>").insertBefore($(this).parent());
} else {
// remove blank element when collapse dropdown
$(this).parent().siblings('.top-blank').slideUp(600);
}
$(this.nextElementSibling).slideToggle(600);
};
});
$(".bottom-button").on("click", function() {
if ($(this.nextElementSibling).css('display') == "none") {
$("<div class='blank'><span></span></div>").insertAfter($(this).parent());
} else {
// remove inserted stuff
$('.blank').slideUp(500);
}
$(this.nextElementSibling).slideToggle(600);
});
and the CSS:
.blank {
position: relative;
min-height: 60px;
border-left: 2px solid white;
left: -50px;
}
.top-blank {
position: relative;
min-height: 60px;
}
updated Codepen working example: http://codepen.io/andrewcockerham/pen/xjgkL/
Not sure if this is the best or most proper way, but it works in my case. Interested to hear if there are other better solutions for this.

Categories