Ordering a graphical list via mouse dragging using JavaScript - javascript

NOTE: Exact description of question follows CSS below. Sample code can be seen in this fiddle.
I have a parent div with a list of child divs within it, that looks like the following:
HTML for said container and children is:
<div class="categories_container">
<div class="category one">One</div>
<div class="category two">Two</div>
<div class="category three">Three</div>
<div class="category four">Four</div>
<div class="category five">Five</div>
<div class="category six">Six</div>
</div>
Where the classes .one, .two, .three, etc... are their relative position in the list.
The children elements are positioned with absolute positioning, within their parent.
CSS as follows (some properties not shown for simplicity):
.categories_container {
height: 324px;
width: 100%;
position: relative;
}
.category {
height: 50px;
width: 98%;
position: absolute;
left: 0px;
z-index: 0;
}
.one {
top: 0px;
}
.two {
top: 54px;
}
.three {
top: 108px;
}
.four {
top: 162px;
}
.five {
top: 216px;
}
.six {
top: 270px;
}
As can be seen in this fiddle, you can click (and hold) on any one of the child elements and move it up and down within the parent div. When you release the mouse, the selected child snaps back to its original position.
Question:
How can I detect if the selected element has been dragged overtop of another? I don't only want to know if they are overlapping, but would like to put a range on it. Something like...
if(center of current child is overtop a set range within another child){
do stuff...
}
What I'd like to do for now (as a proof of concept) is to have the underneath child's background color change WHILE the vertical center of the selected child is within the range 0.4-0.6 of the bottom child's height. If the selected child is dragged out of said region, the background should change back.
I've tried something like:
$('.category').mouseover(function(){
if(dragging){
... execute code...
}
});
But it seems that if I am dragging one element over the other, the bottom element cannot see the mouse, and so the function is never executed.
Also:
I've tried a few different methods to keep the cursor as a pointer while dragging, but no matter what it switches to the text cursor whilst dragging. So any help with that would also be appreciated.
For the pointer thing I've tried putting $(this).css('cursor', 'pointer'); in the mousedown and mouse move functions, but to no avail.
Thanks in advance! Sorry if any of this is confusing.

Here is the solution I came up with, purely with JS and JQuery, with no external libraries required and without using JQueryUI Sortables.
HTML:
<div class="list_container">
<div class="list_item">One</div>
<div class="list_item">Two</div>
<div class="list_item">Three</div>
<div class="list_item">Four</div>
<div class="list_item">Five</div>
<div class="list_item">Six</div>
</div>
where list_container holds the individual list_item elements. Is it the latter of the two which can be moved around to create your sorted list. You can put just about anything you'd like within list_item and it'll still work just fine.
CSS:
.list_container {
position: relative;
}
.list_item {
position: absolute;
z-index: 0;
left: 0px;
}
.list_item.selected {
z-index: 1000;
}
Please visit this fiddle for the full list of CSS rules (only necessary ones are shown above).
JavaScript:
I'll go through this bit-by-bit and then show the full code at the bottom.
First off, I defined an array that matches up index numbers with their written counterparts
var classes = new Array("one", "two", "three", ...);
This is used to create classes dynamically (upon page load). These classes are used to order the list. You are only required to populate this array with as many items as you will have in your list. This is the one downfall of the code I have written and am unsure of how to overcome this issue (would be VERY tedious to enter in the elements for a list of hundreds of items, or more!)
Next, a few other variables:
var margin = 2; // Space desired between each list item
var $el; // Used to hold the ID of the element that has been selected
var oldPos = 0; // The position of the selected element BEFORE animation
var newPos = 0; // The position of the selected element AFTER animation (also current position)
var dragging = false; // Whether or not an item is being moved
var numElements = $('.list_container > div').length;
// selectionHeight is the height of each list element (assuming all the same height)
// It includes the div height, the height of top and bottom borders, and the desired margin
var selectionHeight = $('.list_container .list_item').height() + parseInt($('.list_container .list_item').css("border-bottom-width")) + parseInt($('.list_container .list_item').css("border-top-width")) + margin;
var classInfo = ''; // classInfo will be populated with the information that is used to dynamically create classes upon page load
When page loads, go through each list_item and assign it a class according to its initial position in the list. Also add to classInfo the location of the TOP of the list item.
$('.list_container .list_item').each(function (index) {
$(this).addClass(classes[index]);
classInfo += '.' + classes[index] + ' {top: ' + index * selectionHeight + 'px;}\n';
});
Now, using classInfo that was created above, dynamically write the classes to the page.
var style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = classInfo;
document.getElementsByTagName('head')[0].appendChild(style);
The above bit of code will write the required classes into the HTML of the page. If you view the source of the page, you can see the classes in the head of the page.
Now for the ordering part. First, mousedown
$('.list_item').mousedown(function (ev) {
$el = $(this);
oldPos = $el.index() + 1;
newPos = oldPos;
dragging = true;
startY = ev.clientY; // Gets the current mouse position
startT = parseInt($el.css('top')); // Gets the current position of the TOP of the item
$el.addClass('selected'); // Adding class brings it to top (z-index) and changes color of list item
});
Next, the mousemove and mouseup functions are tied together
$(window).mousemove(function (ev) { // Use $(window) so mouse can leave parent div and still work
if (dragging) {
$el.attr('class', 'list_item') // Remove the numbered class (.one, .two, etc)
$el.addClass('selected'); // Add this class back for aesthetics
// ----- calculate new top
var newTop = startT + (ev.clientY - startY);
$el.css('cursor', 'pointer');
// ------
//------ stay in parent
var maxTop = $el.parent().height() - $el.height();
newTop = newTop < 0 ? 0 : newTop > maxTop ? maxTop : newTop;
$el.css('top', newTop);
//------
newPos = getPos(newTop, selectionHeight); // Determine what the current position of the selected list item is
// If the position of the list item has changed, move the position's current element out of the way and reassign oldPos to newPos
if (oldPos != newPos) {
moveThings(oldPos, newPos, selectionHeight);
oldPos = newPos;
}
}
}).mouseup(function () {
dragging = false; // User is no longer dragging
$el.removeClass('selected'); // Element is no longer selected
setNewClass($el, newPos); // Set the new class of the moved list item
$el.css('top', (newPos - 1) * selectionHeight); // Position the moved element where it belongs. Otherwise it'll come to rest where you release it, not in its correct position.
});
Finally, the three functions getPos, moveThings and setNewClass are as follows:
function getPos(a, b) { // a == newTop, b == selectionHeight
return Math.round( (a/b) + 1 );
}
getPos works by finding out which region the selected element is currently in. If newTop is less than .5b, then it is in region 1. If between .5b and 1.5b, then it is region 2. If between 1.5b and 2.5b, then in region 3. And so on. Write out a few cases on a piece of paper and it'll make sense what is happening.
function moveThings(a, b, c) { // a == oldPos, b == newPos, c == selectedHeight
var first = classes[b - 1]; // What is the current class of the item that will be moved
var $newEl = $('.list_container .' + first); // ID of element that will be moved
if (a < b) { // oldPos less than newPos
var second = classes[b - 2]; // The new class of the moved element will be one less
var newTop = parseInt($newEl.css('top')) - c; // Top of element will move up
} else { // oldPos more than newPos
var second = classes[b]; // The new class of the moved element will be one more
var newTop = parseInt($newEl.css('top')) + c; // Top of element will move down
}
// The following line of code is required, otherwise the following animation
// will animate of from top=0px to the new position (opposed to from top=currentPosition)
// Try taking it out and seeing
$newEl.css('top', parseInt($newEl.css('top')));
$newEl.removeClass(first); // Remove the current numbered class of element to move
// Move element and remove the added style tags (or future animations will get buggy)
$newEl.animate({top: newTop}, 300, function () {
$newEl.removeAttr('style');
});
$newEl.addClass(second); // Add the new numbered class
return false; // Cleans up animations
}
The function above is what does the actual animation part and moves the list items around to accommodate the selected list item.
function setNewClass(e, a) { // e == selected element, a == newPos
// Remove 'selected' class, then add back the 'list_item' class and the new numbered class
e.attr('class', 'list_item').addClass(classes[a-1]);
}
** All JavaScript together: **
var classes = new Array("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeem", "eighteen", "nineteen", "twenty", "twentyone", "twentytwo", "twentythree", "twentyfour");
$(document).ready(function () {
var margin = 2;
var $el;
var oldPos = 0;
var newPos = 0;
var dragging = false;
var selectionHeight = $('.list_container .list_item').height() + parseInt($('.list_container .list_item').css("border-bottom-width")) + parseInt($('.list_container .list_item').css("border-top-width")) + margin;
var classInfo = '';
$('.list_container .list_item').each(function (index) {
$(this).addClass(classes[index]);
classInfo += '.' + classes[index] + ' {top: ' + index * selectionHeight + 'px;}\n';
});
var style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = classInfo;
document.getElementsByTagName('head')[0].appendChild(style);
$('.list_item').mousedown(function (ev) {
$el = $(this);
oldPos = $el.index() + 1;
newPos = oldPos;
dragging = true;
startY = ev.clientY;
startT = parseInt($el.css('top'));
$el.addClass('selected');
});
$(window).mousemove(function (ev) {
if (dragging) {
$el.attr('class', 'list_item')
$el.addClass('selected');
// ----- calculate new top
var newTop = startT + (ev.clientY - startY);
$el.css('cursor', 'pointer');
// ------
//------ stay in parent
var maxTop = $el.parent().height() - $el.height();
newTop = newTop < 0 ? 0 : newTop > maxTop ? maxTop : newTop;
$el.css('top', newTop);
//------
newPos = getPos(newTop, selectionHeight);
if (oldPos != newPos) {
moveThings(oldPos, newPos, selectionHeight);
oldPos = newPos;
}
}
}).mouseup(function () {
dragging = false;
$el.removeClass('selected');
setNewClass($el, newPos);
$el.css('top', (newPos - 1) * selectionHeight);
});
});
function getPos(a, b) { // a == topPos, b == selectionHeight
return Math.round((a / b) + 1);
}
function moveThings(a, b, c) { // a == oldPos, b == newPos, c == selectedHeight
var first = classes[b - 1];
var $newEl = $('.list_container .' + first);
if (a < b) { // oldPos less than newPos
var second = classes[b - 2];
var newTop = parseInt($newEl.css('top')) - c;
} else { // oldPos more than newPos
var second = classes[b];
var newTop = parseInt($newEl.css('top')) + c;
}
$newEl.css('top', parseInt($newEl.css('top')));
$newEl.removeClass(first);
$newEl.animate({
top: newTop
}, 300, function () {
$newEl.removeAttr('style');
});
$newEl.addClass(second);
return false; // Cleans up animations
}
function setNewClass(e, a) { // e == selected element, a == newPos
e.attr('class', 'list_item').addClass(classes[a - 1]);
}

Related

Simple Scrollspy without Bootstrap on custom Nav Component

I have a Table of Contents with id #TableOfContents in which each href points to a h2 or h3.
The problem I am having is that once the Heading, h2 or h3 is observed by intersection observer, Entry for that is highlighted by adding class side-active for that link in #TableOfContents, but as soon as the long content (such as p paragraph) after the heading comes in viewport the highlight for that section is removed since the heading is not in viewport.
This is a problem since I want the section (h2, h3) to still be highlighted until next h2 or h3 doesn't cross half of viewport.
What can I do?
window.addEventListener('DOMContentLoaded', () => {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
const id = entry.target.getAttribute('id');
if (entry.intersectionRatio > 0) {
document.querySelector(`#TableOfContents a[href="#${id}"]`).classList.add('side-active');
} else {
document.querySelector(`#TableOfContents a[href="#${id}"]`).classList.remove('side-active');
}
});
});
toc = document.querySelectorAll('#TableOfContents a');
// get content so that link refer to it
toc.forEach(function (link) {
var id = link.getAttribute("href");
var element = document.querySelector(id);
observer.observe(element);
});
});
Text is highlighted when heading in viewport
Text is not highlighted once heading is not in viewport
So the thing was we needed to calculate containerBottom by:
adding current container's top + next container's top
Or if it is last container then,
current container's top + it's outerHeight()
Once that; if ith container is in scrollposition add class to it And remove class from all containers previous to ith container (line no 18-21) (reverse loop)
$(function () {
// Table of contents (`ul`) that contains `a` tag in `li` which we want to highlight
var sectionIds = $('#TableOfContents a');
$(document).on('scroll', function(){
sectionIds.each(function(i, e){
var container = $(this).attr('href');
var containerOffset = $(container).offset().top; // container's top
var nextContainer = $(sectionIds[i+1]).attr('href')
if (i != sectionIds.length-1) {
// if this container isn't last container
var containerHeight = $(nextContainer).offset().top;
} else {
// last container's height will be outerHeight
var containerHeight = $(container).outerHeight();
}
var containerBottom = containerOffset + containerHeight;
var scrollPosition = $(document).scrollTop();
if(scrollPosition < containerBottom - 20 && scrollPosition >= containerOffset - 20){
for (var j = i; j >= 0; j--) {
$(sectionIds[j]).removeClass('active');
}
$(sectionIds[i]).addClass('active');
} else{
$(sectionIds[i]).removeClass('active');
}
});
});
});
Check the first heading ("Line Equations" h1) that is highlighted stays highlighted even though the title is not in viewport until next sub heading ("Distance of a point from a line") comes into view.

How to change the img of a cloned div onClick

Right now I have a lovely code (that was initially for flowing snowflakes) that lets cloned divs fall from the top of the window, to the bottom and repeat. The thing is that I want the content to change on click. However, this is not yet working.
I know how to change content of a div without it being cloned, but I have serious trouble figuring out how to do the same with one div being cloned within the code.
Anyone have any tips what I should do, even a hint in which direction to go. I'm clueless.
This is the fiddle:
http://jsfiddle.net/4yaxvt7h/
the javascript:
function changeImage(){
document.getElementById('toChange').src='https://i.pinimg.com/originals/6e/19/56/6e195649034f042d1dea5230234570a8.gif';
}
// Array to store our Snowflake objects
var snowflakes = [];
// Global variables to store our browser's window size
var browserWidth;
var browserHeight;
// Specify the number of snowflakes you want visible
var numberOfSnowflakes = 15;
// Flag to reset the position of the snowflakes
var resetPosition = false;
// Handle accessibility
var enableAnimations = false;
var reduceMotionQuery = matchMedia("(prefers-reduced-motion)");
// Handle animation accessibility preferences
function setAccessibilityState() {
if (reduceMotionQuery.matches) {
enableAnimations = false;
} else {
enableAnimations = true;
}
}
setAccessibilityState();
reduceMotionQuery.addListener(setAccessibilityState);
//
// It all starts here...
//
function setup() {
if (enableAnimations) {
window.addEventListener("DOMContentLoaded", generateSnowflakes, false);
window.addEventListener("resize", setResetFlag, false);
}
}
setup();
//
// Constructor for our Snowflake object
//
function Snowflake(element, speed, xPos, yPos) {
// set initial snowflake properties
this.element = element;
this.speed = speed;
this.xPos = xPos;
this.yPos = yPos;
this.scale = 1;
// declare variables used for snowflake's motion
this.counter = 0;
this.sign = Math.random() < 0.5 ? 1 : -1;
// setting an initial opacity and size for our snowflake
this.element.style.opacity = 1;
}
//
// The function responsible for actually moving our snowflake
//
Snowflake.prototype.update = function () {
// using some trigonometry to determine our x and y position
this.counter += this.speed / 5000;
this.xPos += this.sign * this.speed * Math.cos(this.counter) / 40;
this.yPos += Math.sin(this.counter) / 40 + this.speed / 30;
this.scale = .5 + Math.abs(10 * Math.cos(this.counter) / 20);
// setting our snowflake's position
setTransform(Math.round(this.xPos), Math.round(this.yPos), this.scale, this.element);
// if snowflake goes below the browser window, move it back to the top
if (this.yPos > browserHeight) {
this.yPos = -50;
}
}
//
// A performant way to set your snowflake's position and size
//
function setTransform(xPos, yPos, scale, el) {
el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0) scale(${scale}, ${scale})`;
}
//
// The function responsible for creating the snowflake
//
function generateSnowflakes() {
// get our snowflake element from the DOM and store it
var originalSnowflake = document.querySelector(".snowflake");
// access our snowflake element's parent container
var snowflakeContainer = originalSnowflake.parentNode;
snowflakeContainer.style.display = "block";
// get our browser's size
browserWidth = document.documentElement.clientWidth;
browserHeight = document.documentElement.clientHeight;
// create each individual snowflake
for (var i = 0; i < numberOfSnowflakes; i++) {
// clone our original snowflake and add it to snowflakeContainer
var snowflakeClone = originalSnowflake.cloneNode(true);
snowflakeContainer.appendChild(snowflakeClone);
// set our snowflake's initial position and related properties
var initialXPos = getPosition(50, browserWidth);
var initialYPos = getPosition(50, browserHeight);
var speed = 5 + Math.random() * 40;
// create our Snowflake object
var snowflakeObject = new Snowflake(snowflakeClone,
speed,
initialXPos,
initialYPos);
snowflakes.push(snowflakeObject);
}
// remove the original snowflake because we no longer need it visible
snowflakeContainer.removeChild(originalSnowflake);
moveSnowflakes();
}
//
// Responsible for moving each snowflake by calling its update function
//
function moveSnowflakes() {
if (enableAnimations) {
for (var i = 0; i < snowflakes.length; i++) {
var snowflake = snowflakes[i];
snowflake.update();
}
}
// Reset the position of all the snowflakes to a new value
if (resetPosition) {
browserWidth = document.documentElement.clientWidth;
browserHeight = document.documentElement.clientHeight;
for (var i = 0; i < snowflakes.length; i++) {
var snowflake = snowflakes[i];
snowflake.xPos = getPosition(50, browserWidth);
snowflake.yPos = getPosition(50, browserHeight);
}
resetPosition = false;
}
requestAnimationFrame(moveSnowflakes);
}
//
// This function returns a number between (maximum - offset) and (maximum + offset)
//
function getPosition(offset, size) {
return Math.round(-1 * offset + Math.random() * (size + 2 * offset));
}
//
// Trigger a reset of all the snowflakes' positions
//
function setResetFlag(e) {
resetPosition = true;
}
html
<div id="snowflakeContainer">
<div class="snowflake"><img src="element1.png"
</div>
css
body{
background-color: black;
}
#snowflakeContainer {
position: absolute;
left: 0px;
top: 0px;
display: none;
}
.snowflake {
position: fixed;
background-color: red;
user-select: none;
z-index: 1000;
pointer-events: none;
border-radius: 50%;
width: 200px;
height: 200px;
}
img{
max-width: 100%;
}
any tips are super welcome!!
You have several different problems here. First, your CSS sets pointer-events: none; for all .snowflake elements. This will stop any clicks from triggering, so you have to remove it if you want mouse interactivity.
Second, your changeImage() function uses document.getElementById('toChange') to get the element to change the source of. No such element exists and even if it did, that would mean that clicking on any of the snowflakes would just change the source of that one image. You need to reference the snowflake that you clicked on. An easy way to do this is by passing this as an argument to the function in the onclick attribute:
HTML:
<div class="snowflake"><img src="https://vignette.wikia.nocookie.net/dragonballfanon/images/7/70/Random.png/revision/latest?cb=20161221030547" onclick="changeImage(this)"></div>
JS:
function changeImage(img) {
img.src = 'https://i.pinimg.com/originals/6e/19/56/6e195649034f042d1dea5230234570a8.gif';
}
Finally, your JSFiddle is configured to delay execution of the JavaScript until after the DOM is ready. This means that the code will be wrapped in a callback function, and therefore the changeImage() function will no longer be in the global scope. This makes it inaccessible to the elements that want to call it in their handlers (the snowflakes). Because you are already listening for the DOMContentLoaded event on your own, you can just change the code to execute normally and it should work. Click on the dropdown that says "JavaScript + jQuery 3.2.1" above your JS code and change the load type to any "No wrap" option and it should work.

Scrolling upwards

I want to make a function that will automatically display items as the user scrolls upwards. Think messages, where older ones are up top, but we start at the bottom with the newest ones. I have another function that loads items at the bottom of a container, and as it does so, the current items remain in position, and scrollbar is smaller. We are not scrolled to the bottom. However, with the other direction, when I prepend items to an array, it scrolls all the way to the top, displaying the top of the loaded items, instead of remaining in the same place and letting the user scroll up as needed.
My code for the bottom scroll is this:
attachScrollWatcher: function (element, offset, callback) {
console.log('scrolling');
var contentHeight = element.scrollHeight;
var yOffset = element.scrollTop;
var y = yOffset + element.offsetHeight;
if (y >= ( contentHeight - offset ))
{
callback();
}
}
This function is attached to an object's onscroll event. However, now I need to make a function that does the opposite, going upwards. Any ideas how this can be implemented?
Basically, when scrollTop === 0 then you're at the top and you need to load a new item..
attachScrollWatcher: function (element, offset, callback) {
if(!element.scrollHeight) callback();
}
The problem is, loading a new item will keep the scrollTop at zero, so the user would have to scroll down and then scroll back up in order for the callback to be triggered again. So, what you wanna do is calculate the scrollHeight before the new item is added and then again after the item is added and then manually set the scrollTop to the difference between the original and the new scrollHeight.
Check out my example attachScrollListener method below...
class upScroller{
constructor(ele = document.body){
this.renderedItems = 0;
this.ele = ele; var i=0;
this.initBoxContents();
}
initBoxContents(){
if(this.ele.scrollHeight == this.ele.clientHeight)
this.populateNextItem().then(()=>this.initBoxContents());
else{
this.ele.scrollTop = this.ele.clientHeight;
this.attachScrollListener();
}
}
getNextItem(){
// Do something here to get the next item to render
// preferably using ajax but I'm using setTimeout
// to emulate the ajax call.
return new Promise(done=>setTimeout(()=>{
this.renderedItems++;
done(`<p>This is paragraph #${this.renderedItems}</p>`);
},50));
}
populateNextItem(){
return new Promise(done=>{
this.getNextItem().then(item=>{
this.ele.insertAdjacentHTML('afterbegin', item);
done();
});
});
}
attachScrollListener(){
this.ele.addEventListener('scroll', ()=>{
if(this.ele.scrollTop) return;
var sh = this.ele.scrollHeight;
this.populateNextItem().then(()=>{
this.ele.scrollTop = this.ele.scrollHeight - sh;
});
});
}
}
var poop = document.getElementById('poop');
new upScroller(poop);
#poop{ height: 300px; overflow: auto; border:1px solid black;}
<div id=poop></div>
I've posted this here as well....
Something like this may work.
attachScrollWatcher: function (element, offset, callback) {
console.log('scrolling');
var contentHeight = element.scrollHeight;
var yOffset = element.scrollTop;
var y = yOffset + element.offsetHeight;
if (y >= ( contentHeight - offset ))
{
callback();
} else {
callbackGoingUp();
}
}

Menu items changing color according to position of element

My page is divided into three sections and each section can be accessed by respective menu item. I am trying to achieve this in Javascript: when the user has reached any of the sections by scrolling, the font color of respective menu item should change.
Here I call the function:
<body onscroll="detectScroll(); showPosition();">
This is the function that detects scrolling and changes some items accordingly. It's working fine:
function detectScroll() {
var header = document.querySelector(".headerOrig"),
header_height = getComputedStyle(header).height.split('px')[0],
header_class = "changeHeader",
logo = document.getElementById("logo")
;
if( window.pageYOffset > (parseInt(header_height) + 500)) {
header.classList.add(header_class);
logo.src = "images/logo2.png";
}
if( window.pageYOffset < (parseInt(header_height) + 500)) {
header.classList.remove(header_class);
logo.src = "images/logo1.png";
}
}
This JS function returns the position of an element. Works fine as well:
function getPosition(element) {
var xPosition = 0;
var yPosition = 0;
while(element) {
xPosition += (element.offsetLeft - element.scrollLeft + element.clientLeft);
yPosition += (element.offsetTop - element.scrollTop + element.clientTop);
element = element.offsetParent;
}
return { x: xPosition, y: yPosition };
}
And, finally, this is the JS function that is being called when scrolling:
function showPosition() {
var myElement = document.getElementById("posBIKES");
var position = getPosition(myElement);
var bike = document.getElementById("bikesMenu");
//alert("The element is located at: " + position.x + ", " + position.y);
if(window.pageYOffset < position.y) {
window.getElementById("bikesMenu").classList.remove("changeMenu");
}
if(window.pageYOffset > position.y) {
window.getElementById("bikesMenu").classList.add("changeMenu");
}
}
The problem is everything works fine until I try to add or remove the class to the item selected (the last function). Any other statement works fine, for example, I tried putting alert("something"); in both conditions and both worked as desired. Whats wrong with adding and removing classes then?
And yes, I have checked the corresponding names and IDs of everything like million times, so theres no issue with that.
Any help is more than appreciated.
Thanks

Javascript: Let user select an HTML element like Firebug?

I want to write a browser (Chrome/FF) extension that needs to select an element on a web page. I would like it to behave like Firebug's element inspector does. You click the inspect arrow and you can then hover/highlight elements. When you click on the element you want, the element is inspected. I'm just interested in the code to allow a user to select an element - not in actually inspecting it or anything similar.
Because I'm writing an extension, it might be nice if you could provide non-jQuery/Prototype/etc.. code so I don't have to distribute that.
I have recently required such a feature for a project I was working on, turned out that I had to use for sides to create a box because otherwise the event.target when you move the mouse would end up being the selector, and if I were to use z-index: -1 it would be a bit fishy when you have a lot of elements that overlap...etc.
Here is a version that I have converted from my project for your benefit, it involves jQuery but it is extremely simple to convert to vanilla as only the mousemove & css methods from jQuery are used.
Step by step instructions.
First create the 5 HTMLElements that are required.
<div id="selector">
<div id="selector-top"></div>
<div id="selector-left"></div>
<div id="selector-right"></div>
<div id="selector-bottom"></div>
</div>
Secondly create a mousemove event on the document (or your container)
$(document).mousemove(function(event) { ... });
Then inside the mousemove we will do some basic checking to prevent selecting the HTML, BODY, selector
var id = event.target.id, tagName = event.target.tagName;
if(id.indexOf('selector') !== -1 || tagName === 'BODY' || tagName === 'HTML') {
return;
}
Then we need to create a object to store our elements like so.
var elements = {
top: $('#selector-top'),
left: $('#selector-left'),
right: $('#selector-right'),
bottom: $('#selector-bottom')
};
After that we store some variables that hold some information about the target element like so.
var $target = event.target;
targetOffset = $target.getBoundingClientRect(),
targetHeight = targetOffset.height,
targetWidth = targetOffset.width;
Then all we do is calculate the position & height for all 4 sides of the selector like so.
elements.top.css({
left: (targetOffset.left - 4),
top: (targetOffset.top - 4),
width: (targetWidth + 5)
});
elements.bottom.css({
top: (targetOffset.top + targetHeight + 1),
left: (targetOffset.left - 3),
width: (targetWidth + 4)
});
elements.left.css({
left: (targetOffset.left - 5),
top: (targetOffset.top - 4),
height: (targetHeight + 8)
});
elements.right.css({
left: (targetOffset.left + targetWidth + 1),
top: (targetOffset.top - 4),
height: (targetHeight + 8)
});
All of the +aFewPixels is just a little optimization so that there is like 2px gap in between the selector and the target.
For the CSS this is what I have come up with.
#selector-top, #selector-bottom {
background: blue;
height:3px;
position: fixed;
transition:all 300ms ease;
}
#selector-left, #selector-right {
background: blue;
width:3px;
position: fixed;
transition:all 300ms ease;
}
The transition gives the selector a very nice sliding effect.
Try out a demo http://jsfiddle.net/rFc8E/9/
Note: This also works for transform: scale(2); eg. when a element is scaled in size.
Edit: I've just updated this, I noticed that the elements object was inside the event handler, I've moved it outside in the demo, this is quite an important performance improvement because now, the elements object is only created once instead of Hundreds of Thousands if not millions of times inside the mousemove event.
I wrote an implementation of this using jQuery as a component of another project. The source and documentation are available here: https://github.com/andrewchilds/jQuery.DomOutline
One simple way to do it is to use an outline instead of a border:
.highlight { outline: 4px solid #07C; }
Just add and remove that class to any element you want to select/deselect (code below is not properly tested):
document.body.addEventListener("mouseover", function(e) {
e.stopPropagation();
e.target.addEventListener("mouseout", function (e) {
e.target.className = e.target.className.replace(new RegExp(" highlight\\b", "g"), "");
});
e.target.className += " highlight";
});
Since you are using an outline, (which is supported by Chrome) instead of a border, elements will not jump around. I'm using something similar in my EasyReader Extension.
HTML Element Picker (Vanilla JS)
Pick and highlight any HTML element on a page with only Vanilla JS! Tested in Chrome, FF, and Opera, doesn't work in IE.
How it works:
What you need is actually very simple. You can just create an empty div box with a background in JS and move it around to highlight on top of hovered elements.
Here's the JS code:
const hoverBox = document.createElement("div");
console.log("hoverBox: ", hoverBox);
hoverBox.style.position = "absolute";
// change to whatever highlight color you want
hoverBox.style.background = "rgba(153, 235, 255, 0.5)";
// avoid blocking the hovered element and its surroundings
hoverBox.style.zIndex = "0";
document.body.appendChild(hoverBox);
let previousTarget;
document.addEventListener("mousemove", (e) => {
let target = e.target;
if (target === hoverBox) {
// the truely hovered element behind the added hover box
const hoveredElement = document.elementsFromPoint(e.clientX, e.clientY)[1];
if (previousTarget === hoveredElement){
// avoid repeated calculation and rendering
return;
} else{
target = hoveredElement;
}
} else{
previousTarget = target;
}
const targetOffset = target.getBoundingClientRect();
const targetHeight = targetOffset.height;
const targetWidth = targetOffset.width;
// add a border around hover box
const boxBorder = 5;
hoverBox.style.width = targetWidth + boxBorder * 2 + "px";
hoverBox.style.height = targetHeight + boxBorder * 2 + "px";
// need scrollX and scrollY to account for scrolling
hoverBox.style.top = targetOffset.top + window.scrollY - boxBorder + "px";
hoverBox.style.left = targetOffset.left + window.scrollX - boxBorder + "px";
});
See Demo
I also made an npm package for the element picker with many more user configurations like background color, border width, transition, etc.
Here's the GitHub page.
I ended up asking in the Firebug group and got some great help:
http://groups.google.com/group/firebug/browse_thread/thread/7d4bd89537cd24e7/2c9483d699efe257?hl=en#2c9483d699efe257
Here is a library that written in pure javascript as an alternative.
TheRoom JS: https://github.com/hsynlms/theroomjs
// theroom information template for target element
var template="";
template += "<div id=\"theroom-info\">";
template += " <span id=\"theroom-tag\"><\/span>";
template += " <span id=\"theroom-id\"><\/span>";
template += " <span id=\"theroom-class\"><\/span>";
template += "<\/div>";
template += "";
template += "<style>";
template += " #theroom-info {";
template += " position: fixed;";
template += " bottom: 0;";
template += " width: 100%;";
template += " left: 0;";
template += " font-family: \"Courier\";";
template += " background-color: #ffffff;";
template += " padding: 10px;";
template += " color: #333333;";
template += " text-align: center;";
template += " box-shadow: 0px 4px 20px rgba(0,0,0,0.3);";
template += " }";
template += "";
template += " #theroom-tag {";
template += " color: #C2185B;";
template += " }";
template += "";
template += " #theroom-id {";
template += " color: #5D4037;";
template += " }";
template += "";
template += " #theroom-class {";
template += " color: #607D8B;";
template += " }";
template += "<\/style>";
var options = {
template: template,
showInfo: true
};
// initialize
theRoom.start(options);
codepen demo
There was a similar question asked on Stackoverflow and it had lots of good answers:
Does anyone know a DOM inspector javascript library or plugin?
For those who are looking for a quick and dirty solution:
http://userscripts.org/scripts/review/3006 is the easiest. Just put the code within <script></script> tags and you are good to go.
https://github.com/josscrowcroft/Simple-JavaScript-DOM-Inspector/blob/master/inspector.js is slightly better and still very easy to integrate in.
For a more sophisticated element inspector, you might want to check out the SelectorGadget as pointed by Udi. The inspector selection code is in http://www.selectorgadget.com/stable/lib/interface.js
Also check this one out:
http://rockingcode.com/tutorial/element-dom-tree-jquery-plugin-firebug-like-functionality/
I found it pretty insightful.. and there's a demo here:
http://rockingcode.com/demos/elemtree/
Hope this helps.
A very basic implementation can be done very easily without jQuery using .onmouseover and e.target:
var last,
bgc;
document.onmouseover = function(e) {
var elem = e.target;
if (last != elem) {
if (last != null) {
last.classList.remove("hovered");
}
last = elem;
elem.classList.add("hovered");
}
}
With the CSS below if you want the children to change background as well:
.hovered,
.hovered * {
cursor: pointer;
color: black;
background-color: red;
}
Demo
If you want to select elements only near the edges (or select the parent near the edges and the element itself everywhere else) you could use .getBoundingClientRect.
var last;
window.addEventListener("mousemove", function(e) {
if(last) {
last.style.background = ''; // empty is enough to restore previous value
}
var elem = e.target;
if(elem === document.body || elem === document.documentElement) {
return;
}
var bb = elem.getBoundingClientRect();
var xr = e.pageX - bb.left; // x relative to elem
var yr = e.pageY - bb.top; // y relative to elem
var ew = 10; // edge width
if(
xr <= ew
|| xr >= bb.width - ew
|| yr <= ew
|| yr >= bb.height - ew
){
elem.style.background = 'red';
last = elem;
}
});
Paired with some borders, this can be pretty usable for selection. Demo
What you need to do is to create 4 elements for the highlighting. They will form an empty square, and so your mouse events are free to fire. This is similar to this overlay example I've made.
The difference is that you only need the four elements (no resize markers), and that the size and position of the 4 boxes are a bit different (to mimick the red border). Then you can use event.target in your event handler, because it gets the real topmost element by default.
Another approach is to hide the exra element, get elementFromPoint, calculate then put it back.
They're faster than light, I can tell you. Even Einstein would agree :)
1.) elementFromPoint overlay/borders - [Demo1] FF needs v3.0+
var box = $("<div class='outer' />").css({
display: "none", position: "absolute",
zIndex: 65000, background:"rgba(255, 0, 0, .3)"
}).appendTo("body");
var mouseX, mouseY, target, lastTarget;
// in case you need to support older browsers use a requestAnimationFrame polyfill
// e.g: https://gist.github.com/paulirish/1579671
window.requestAnimationFrame(function frame() {
window.requestAnimationFrame(frame);
if (target && target.className === "outer") {
box.hide();
target = document.elementFromPoint(mouseX, mouseY);
}
box.show();
if (target === lastTarget) return;
lastTarget = target;
var $target = $(target);
var offset = $target.offset();
box.css({
width: $target.outerWidth() - 1,
height: $target.outerHeight() - 1,
left: offset.left,
top: offset.top
});
});
$("body").mousemove(function (e) {
mouseX = e.clientX;
mouseY = e.clientY;
target = e.target;
});
2.) mouseover borders - [Demo2]
var box = new Overlay();
$("body").mouseover(function(e){
var el = $(e.target);
var offset = el.offset();
box.render(el.outerWidth(), el.outerHeight(), offset.left, offset.top);
});​
/**
* This object encapsulates the elements and actions of the overlay.
*/
function Overlay(width, height, left, top) {
this.width = this.height = this.left = this.top = 0;
// outer parent
var outer = $("<div class='outer' />").appendTo("body");
// red lines (boxes)
var topbox = $("<div />").css("height", 1).appendTo(outer);
var bottombox = $("<div />").css("height", 1).appendTo(outer);
var leftbox = $("<div />").css("width", 1).appendTo(outer);
var rightbox = $("<div />").css("width", 1).appendTo(outer);
// don't count it as a real element
outer.mouseover(function(){
outer.hide();
});
/**
* Public interface
*/
this.resize = function resize(width, height, left, top) {
if (width != null)
this.width = width;
if (height != null)
this.height = height;
if (left != null)
this.left = left;
if (top != null)
this.top = top;
};
this.show = function show() {
outer.show();
};
this.hide = function hide() {
outer.hide();
};
this.render = function render(width, height, left, top) {
this.resize(width, height, left, top);
topbox.css({
top: this.top,
left: this.left,
width: this.width
});
bottombox.css({
top: this.top + this.height - 1,
left: this.left,
width: this.width
});
leftbox.css({
top: this.top,
left: this.left,
height: this.height
});
rightbox.css({
top: this.top,
left: this.left + this.width - 1,
height: this.height
});
this.show();
};
// initial rendering [optional]
// this.render(width, height, left, top);
}

Categories

Resources