Mapbox GL - highlight features and queryRenderedFeatures() while allowing basemap style change - javascript

I need to be able to highlight features in a bounding box using Mapbox GL as shown in this example. My map also needs to have the ability to change the style layer, e.g. changing the base style from mapbox://styles/mapbox/light-v10 to mapbox://styles/mapbox/satellite-v9. This has been challenging because Mapbox GL does not natively support the concept of "basemaps" like, e.g. leaflet does.
My solution, after much searching (I believe I eventually followed a workaround posted in a github issue) involved:
Using map.on('style.load') instead of map.on('load') as shown in the example (I believe map.on('style.load') is not part of their public API, but I could be wrong) and,
Iterating through an array of sources/layers (see the vectorTileLayers variable in the code below) to add vector tiles layers to the map every time a new style is loaded.
This works brilliantly for me in some ways - I'm able to add an arbitrary number of vector tile sources to the array, and they are all added back to the map if the base style is changed. However, I am unable to add a query feature following the example provided by Mapbox if the sources/layers are added to the map in an array and iterated through as shown below. The bounding feature works, but when it is drawn, and released, I see the error show by running the snippet below.
This is a simplified version my actual problem that will allow others to run and manipulate the code. In reality, I'm adding my own vector tile layers (stored in the vectorTileLayers array, which works without issue. When I try and add another layer with the same source and different styling, as shown in the example, I'm unable to actually query the desired layer. Any help would be greatly appreciated. Just FYI - the toggleable layers buttons are not showing up in this snippet, but it's not critical to solve the problem.
mapboxgl.accessToken = 'pk.eyJ1IjoiamFtZXljc21pdGgiLCJhIjoiY2p4NTRzdTczMDA1dzRhbXBzdmFpZXV6eCJ9.-k7Um-xmYy4xhNDN6kDvpg';
var map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/light-v10',
center: [-98, 38.88],
minZoom: 2,
zoom: 3
});
var vectorTileLayers = [{
source: {
type: 'vector',
url: 'mapbox://mapbox.82pkq93d'
},
layer: {
id: 'counties',
type: 'fill',
source: 'counties',
'source-layer': 'original',
paint: {
'fill-outline-color': 'rgba(0,0,0,0.1)',
'fill-color': 'rgba(0,0,0,0.1)'
},
}
},
{
source: {
type: 'vector',
url: 'mapbox://mapbox.82pkq93d'
},
layer: {
id: 'counties-highlighted',
type: 'fill',
source: 'counties',
'source-layer': 'original',
paint: {
'fill-outline-color': '#484896',
'fill-color': '#6e599f',
'fill-opacity': 0.75
},
filter: ['in', 'FIPS', '']
}
}
]
map.on('style.load', function() {
for (var i = 0; i < vectorTileLayers.length; i++) {
var tileLayer = vectorTileLayers[i];
map.addSource(tileLayer.layer.source, tileLayer.source);
map.addLayer(tileLayer.layer);
}
var layerList = document.getElementById('basemapmenu');
var inputs = layerList.getElementsByTagName('input');
function switchLayer(layer) {
var layerId = layer.target.id;
map.setStyle('mapbox://styles/mapbox/' + layerId);
}
for (var i = 0; i < inputs.length; i++) {
inputs[i].onclick = switchLayer;
}
});
// Disable default box zooming.
map.boxZoom.disable();
// Create a popup, but don't add it to the map yet.
var popup = new mapboxgl.Popup({
closeButton: false
});
var canvas = map.getCanvasContainer();
var start;
var current;
var box;
canvas.addEventListener('mousedown', mouseDown, true);
// Return the xy coordinates of the mouse position
function mousePos(e) {
var rect = canvas.getBoundingClientRect();
return new mapboxgl.Point(
e.clientX - rect.left - canvas.clientLeft,
e.clientY - rect.top - canvas.clientTop
);
}
function mouseDown(e) {
// Continue the rest of the function if the shiftkey is pressed.
if (!(e.shiftKey && e.button === 0)) return;
// Disable default drag zooming when the shift key is held down.
map.dragPan.disable();
// Call functions for the following events
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
document.addEventListener('keydown', onKeyDown);
// Capture the first xy coordinates
start = mousePos(e);
}
function onMouseMove(e) {
// Capture the ongoing xy coordinates
current = mousePos(e);
// Append the box element if it doesnt exist
if (!box) {
box = document.createElement('div');
box.classList.add('boxdraw');
canvas.appendChild(box);
}
var minX = Math.min(start.x, current.x),
maxX = Math.max(start.x, current.x),
minY = Math.min(start.y, current.y),
maxY = Math.max(start.y, current.y);
// Adjust width and xy position of the box element ongoing
var pos = 'translate(' + minX + 'px,' + minY + 'px)';
box.style.transform = pos;
box.style.WebkitTransform = pos;
box.style.width = maxX - minX + 'px';
box.style.height = maxY - minY + 'px';
}
function onMouseUp(e) {
// Capture xy coordinates
finish([start, mousePos(e)]);
}
function onKeyDown(e) {
// If the ESC key is pressed
if (e.keyCode === 27) finish();
}
function finish(bbox) {
// Remove these events now that finish has been called.
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('keydown', onKeyDown);
document.removeEventListener('mouseup', onMouseUp);
if (box) {
box.parentNode.removeChild(box);
box = null;
}
// If bbox exists. use this value as the argument for `queryRenderedFeatures`
if (bbox) {
var features = map.queryRenderedFeatures(bbox, {
layers: ['counties']
});
if (features.length >= 1000) {
return window.alert('Select a smaller number of features');
}
// Run through the selected features and set a filter
// to match features with unique FIPS codes to activate
// the `counties-highlighted` layer.
var filter = features.reduce(
function(memo, feature) {
memo.push(feature.properties.FIPS);
return memo;
},
['in', 'FIPS']
);
map.setFilter('counties-highlighted', filter);
}
map.dragPan.enable();
}
map.on('mousemove', function(e) {
var features = map.queryRenderedFeatures(e.point, {
layers: ['counties-highlighted']
});
// Change the cursor style as a UI indicator.
map.getCanvas().style.cursor = features.length ? 'pointer' : '';
if (!features.length) {
popup.remove();
return;
}
var feature = features[0];
popup
.setLngLat(e.lngLat)
.setText(feature.properties.COUNTY)
.addTo(map);
});
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
#basemapmenu {
position: absolute;
display: inline-block;
background-color: transparent;
bottom: 0;
left: 0;
margin-left: 10px;
margin-bottom: 40px;
}
.boxdraw {
background: rgba(56, 135, 190, 0.1);
border: 2px solid #3887be;
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
}
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet"/>
<link href="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css" rel="stylesheet"/>
<script src="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.js"></script>
<div id="map"></div>
<div id='basemapmenu'>
<input id='light-v10' class='btn btn-outline-primary' type='button' name='rtoggle' value='Light' checked='checked'>
<input id='dark-v10' class='btn btn-outline-primary' type='button' name='rtoggle' value='Dark'>
<input id='satellite-v9' class='btn btn-outline-primary' type='button' name='rtoggle' value='Satellite'>
</div>

I ran you code locally and found the problem!
You are trying to add same source twice and mapbox was giving you error for that, if you check your Console:
Problem: There can be only one source with one ID on Map
Error: There is already a source with this ID
After first loop execute successfully. On second iteration, it fails to add same source twice, code breaks here and doesn't continue further. Hence doesn't add second layer(therefore no functionality works thereafter!)
Fix:(Added a small check to not add same source if already added)
map.on('style.load', function () {
for (var i = 0; i < vectorTileLayers.length; i++) {
var tileLayer = vectorTileLayers[i];
if (!map.getSource(tileLayer.layer.source,)) //FIX
map.addSource(tileLayer.layer.source, tileLayer.source);
map.addLayer(tileLayer.layer);
}
var layerList = document.getElementById('basemapmenu');
var inputs = layerList.getElementsByTagName('input');
function switchLayer(layer) {
var layerId = layer.target.id;
map.setStyle('mapbox://styles/mapbox/' + layerId);
}
for (var i = 0; i < inputs.length; i++) {
inputs[i].onclick = switchLayer;
}
});
I can see the Code is working fine:
Hope this will fix the problem.

I'll leave the above answer from Dolly as accepted because it answers my original question exactly. Unforunately, it didn't exactly solve my actual problem. After some additional fiddling, I wanted to post a fully functional version for reference in case people come across this looking for a solution to adding toggleable "basemap" layers in Mapbox GL with or without user interactivity options like this one.
The solution above does not work after toggling between basemap layers. The vector tile layers are not reloaded properly. The solution below seems to do the trick. It uses map.on('styledata' ...) rather than map.on('style.load' ...), which seems to allow for loading layers by the more conventional method of first calling map.addSource() followed by map.addLayer(). You can load a single source, and then an arbitrary number of layers pointing to that source. So in my "real world" example, I load 3 sources, and 5 layers from those sources.
(FYI - for some reason the built-in snippet tool from stack overflow isn't rendering the basemap buttons. If you copy the code exactly into JS fiddle or codepen it will work)
I hope this helps future humans - it seems like lots of people have had problems with managing Mapbox styles with custom layers on top. I certainly have.
mapboxgl.accessToken =
"pk.eyJ1IjoiamFtZXljc21pdGgiLCJhIjoiY2p4NTRzdTczMDA1dzRhbXBzdmFpZXV6eCJ9.-k7Um-xmYy4xhNDN6kDvpg";
var map = new mapboxgl.Map({
container: "map",
style: "mapbox://styles/mapbox/light-v10",
center: [-98, 38.88],
minZoom: 2,
zoom: 3
});
map.on("styledata", function() {
map.addSource("counties", {
type: "vector",
url: "mapbox://mapbox.82pkq93d"
});
map.addLayer({
id: "counties",
type: "fill",
source: "counties",
"source-layer": "original",
paint: {
"fill-outline-color": "rgba(0,0,0,0.1)",
"fill-color": "rgba(0,0,0,0.1)"
}
});
map.addLayer({
id: "counties-highlighted",
type: "fill",
source: "counties",
"source-layer": "original",
paint: {
"fill-outline-color": "#484896",
"fill-color": "#6e599f",
"fill-opacity": 0.75
},
filter: ["in", "FIPS", ""]
});
var layerList = document.getElementById("basemapmenu");
var inputs = layerList.getElementsByTagName("input");
function switchLayer(layer) {
var layerId = layer.target.id;
map.setStyle("mapbox://styles/mapbox/" + layerId);
}
for (var i = 0; i < inputs.length; i++) {
inputs[i].onclick = switchLayer;
}
});
// Disable default box zooming.
map.boxZoom.disable();
// Create a popup, but don't add it to the map yet.
var popup = new mapboxgl.Popup({
closeButton: false
});
var canvas = map.getCanvasContainer();
var start;
var current;
var box;
canvas.addEventListener("mousedown", mouseDown, true);
// Return the xy coordinates of the mouse position
function mousePos(e) {
var rect = canvas.getBoundingClientRect();
return new mapboxgl.Point(
e.clientX - rect.left - canvas.clientLeft,
e.clientY - rect.top - canvas.clientTop
);
}
function mouseDown(e) {
// Continue the rest of the function if the shiftkey is pressed.
if (!(e.shiftKey && e.button === 0)) return;
// Disable default drag zooming when the shift key is held down.
map.dragPan.disable();
// Call functions for the following events
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
document.addEventListener("keydown", onKeyDown);
// Capture the first xy coordinates
start = mousePos(e);
}
function onMouseMove(e) {
// Capture the ongoing xy coordinates
current = mousePos(e);
// Append the box element if it doesnt exist
if (!box) {
box = document.createElement("div");
box.classList.add("boxdraw");
canvas.appendChild(box);
}
var minX = Math.min(start.x, current.x),
maxX = Math.max(start.x, current.x),
minY = Math.min(start.y, current.y),
maxY = Math.max(start.y, current.y);
// Adjust width and xy position of the box element ongoing
var pos = "translate(" + minX + "px," + minY + "px)";
box.style.transform = pos;
box.style.WebkitTransform = pos;
box.style.width = maxX - minX + "px";
box.style.height = maxY - minY + "px";
}
function onMouseUp(e) {
// Capture xy coordinates
finish([start, mousePos(e)]);
}
function onKeyDown(e) {
// If the ESC key is pressed
if (e.keyCode === 27) finish();
}
function finish(bbox) {
// Remove these events now that finish has been called.
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("keydown", onKeyDown);
document.removeEventListener("mouseup", onMouseUp);
if (box) {
box.parentNode.removeChild(box);
box = null;
}
// If bbox exists. use this value as the argument for `queryRenderedFeatures`
if (bbox) {
var features = map.queryRenderedFeatures(bbox, {
layers: ["counties"]
});
if (features.length >= 1000) {
return window.alert("Select a smaller number of features");
}
// Run through the selected features and set a filter
// to match features with unique FIPS codes to activate
// the `counties-highlighted` layer.
var filter = features.reduce(
function(memo, feature) {
memo.push(feature.properties.FIPS);
return memo;
}, ["in", "FIPS"]
);
map.setFilter("counties-highlighted", filter);
}
map.dragPan.enable();
}
map.on("mousemove", function(e) {
var features = map.queryRenderedFeatures(e.point, {
layers: ["counties-highlighted"]
});
// Change the cursor style as a UI indicator.
map.getCanvas().style.cursor = features.length ? "pointer" : "";
if (!features.length) {
popup.remove();
return;
}
var feature = features[0];
popup.setLngLat(e.lngLat).setText(feature.properties.COUNTY).addTo(map);
});
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
#basemapmenu {
position: absolute;
display: inline-block;
background-color: transparent;
bottom: 0;
left: 0;
margin-left: 10px;
margin-bottom: 40px;
}
.boxdraw {
background: rgba(56, 135, 190, 0.1);
border: 2px solid #3887be;
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
}
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css" rel="stylesheet" />
<script src="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.js"></script>
<div id="map"></div>
<div id='basemapmenu'>
<input id='light-v10' class='btn btn-outline-primary' type='button' name='rtoggle' value='Light' checked='checked'>
<input id='dark-v10' class='btn btn-outline-primary' type='button' name='rtoggle' value='Dark'>
<input id='satellite-v9' class='btn btn-outline-primary' type='button' name='rtoggle' value='Satellite'>
</div>

Related

How do I see if one element is touching another in JavaScript without clicking anything

OK so I've tried one thing from a different question and it worked, but not the way I wanted it to. it didn't work the way I wanted it to! You literally had to click when two objects were touching so it would alert you, if somebody can figure out a way to detect if two elements are touching without having to click that would be a life saver! So I hope you people who read this request please respond if you know how. this is the code below. so one object is moving and i want it to make it stop when the object hits the player (i am making a game) the movement is by px.... i want it to keep testing if one object hits the player, and if it does i want it to stop everything.
var boxes = document.querySelectorAll('.box');
boxes.forEach(function (el) {
if (el.addEventListener) {
el.addEventListener('click', clickHandler);
} else {
el.attachEvent('onclick', clickHandler);
}
})
var detectOverlap = (function () {
function getPositions(elem) {
var pos = elem.getBoundingClientRect();
return [[pos.left, pos.right], [pos.top, pos.bottom]];
}
function comparePositions(p1, p2) {
var r1, r2;
if (p1[0] < p2[0]) {
r1 = p1;
r2 = p2;
} else {
r1 = p2;
r2 = p1;
}
return r1[1] > r2[0] || r1[0] === r2[0];
}
return function (a, b) {
var pos1 = getPositions(a),
pos2 = getPositions(b);
return comparePositions(pos1[0], pos2[0]) && comparePositions(pos1[1], pos2[1]);
};
})();
function clickHandler(e) {
var elem = e.target,
elems = document.querySelectorAll('.box'),
elemList = Array.prototype.slice.call(elems),
within = elemList.indexOf(elem),
touching = [];
if (within !== -1) {
elemList.splice(within, 1);
}
for (var i = 0; i < elemList.length; i++) {
if (detectOverlap(elem, elemList[i])) {
touching.push(elemList[i].id);
}
}
if (touching.length) {
console.log(elem.id + ' touches ' + touching.join(' and ') + '.');
alert(elem.id + ' touches ' + touching.join(' and ') + '.');
} else {
console.log(elem.id + ' touches nothing.');
alert(elem.id + ' touches nothing.');
}
}
this is my video game right now (please do not copy)
<!DOCTYPE html>
/
<html>
<form id="player" class="box">
</form>
<button type="button" class="up" onclick="moveup()">^</button>
<button type="button" class="down" onclick="movedown()">v
</button>
<style src="style.css">
#player {
width: 300px;
height: 100px;
background-color: blue;
display: inline-block;
position: relative;
bottom: -250px;
left: 200px;
}
.up {
position: relative;
bottom: -400px;
}
.down {
position: relative;
bottom: -420px;
}
body {
background-color: black;
}
#car {
width: 300px;
height: 100px;
background-color: red;
display: inline-block;
position: relative;
bottom: -250px;
left: 600px;
}
</style>
<form id="car" class="box"></form>
<script>
imgObj = document.getElementById('player');
imgObj.style.position= 'relative';
imgObj.style.bottom = '-250px';
function moveup() {
imgObj.style.position= 'relative';
imgObj.style.bottom = '-250px';
imgObj.style.bottom = parseInt(imgObj.style.bottom) + 70 + 'px';
}
function movedown() {
imgObj.style.position= 'relative';
imgObj.style.bottom = '-250px';
imgObj.style.bottom = parseInt(imgObj.style.bottom) + -120 + 'px';
}
myMove();
function myMove() {
var elem = document.getElementById("car");
var pos = 0;
var id = setInterval(frame, 5);
function frame() {
if (pos == 1000) {
clearInterval(id);
myMove();
} else {
pos++;
elem.style.left = pos + "px";
elem.style.left = pos + "px";
}
}
}
/* please do not copy; this is it so far i want the red box when it hits the player(blue box) to stop everything that is happening */
/* made by Jsscripter; car game */
</script>
</html>
Intersection observer. API was largely developed because of news feeds and infinite scrolling. Goal was to solve when something comes into view, load content. Also is a great fit for a game.
The Intersection Observer API lets code register a callback function
that is executed whenever an element they wish to monitor enters or
exits another element (or the viewport), or when the amount by which
the two intersect changes by a requested amount. This way, sites no
longer need to do anything on the main thread to watch for this kind
of element intersection, and the browser is free to optimize the
management of intersections as it sees fit.
https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
All major browsers except safari support the API. For backwards compatibility and Safari support can use the polyfill from W3C found here. Check out this example from MDN:
var callback = function(entries, observer) {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed
// target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
};
var options = {
root: document.querySelector('#scrollArea'),
rootMargin: '0px',
threshold: 1.0
}
var observer = new IntersectionObserver(callback, options);
var target = document.querySelector('#listItem');
observer.observe(target);
See this in action here: https://codepen.io/anon/pen/OqpeMV

Get the focused image element inside a contenteditable div

I need to modify a pasted image inside a contenteditable div: resize it proportionately, add borders, etc... Perhaps this can be achieved by adding a className that will alter the necessary CSS properties.
The problem is that I don't know how to reference the focused, i.e. the active, the clicked-upon image or any element for that matter.
This is the HTML that I am trying to use
<!DOCTYPE html>
<html>
<body>
<div contenteditable="true">This is a div. It is editable. Try to change this text.</div>
</body>
</html>
Using css, html and javascript
Your editable content should be inside a parent div with an id or class name, you can even have different divs for images and such.
Then it is as simple as having css like so:
#editableContentDiv img {
imgProperty: value;
}
Then you can have javascript like so:
function insertImage(){
var $img = document.querySelector("#editableContentDiv img");
$img.setAttribute('class','resize-drag');
}
Eventually if you have more than one img inside the same div you can use querySelectorAll instead of querySelector and then iterate through the img tags inside same div.
I beleive that should at least give you an idea of where to start with what you want.
Similar example
I found this gist that seems to have similar things to want you want but a bit more complicated.
function resizeMoveListener(event) {
var target = event.target,
x = (parseFloat(target.dataset.x) || 0),
y = (parseFloat(target.dataset.y) || 0);
// update the element's style
target.style.width = event.rect.width + 'px';
target.style.height = event.rect.height + 'px';
// translate when resizing from top or left edges
x += event.deltaRect.left;
y += event.deltaRect.top;
updateTranslate(target, x, y);
}
function dragMoveListener(event) {
var target = event.target,
// keep the dragged position in the data-x/data-y attributes
x = (parseFloat(target.dataset.x) || 0) + event.dx,
y = (parseFloat(target.dataset.y) || 0) + event.dy;
updateTranslate(target, x, y);
}
function updateTranslate(target, x, y) {
// translate the element
target.style.webkitTransform =
target.style.transform =
'translate(' + x + 'px, ' + y + 'px)';
// update the position attributes
target.dataset.x = x;
target.dataset.y = y;
}
function insertImage() {
var $img = document.createElement('img');
$img.setAttribute('src', 'https://vignette.wikia.nocookie.net/googology/images/f/f3/Test.jpeg/revision/latest?cb=20180121032443');
$img.setAttribute('class', 'resize-drag');
document.querySelector(".container-wrap").appendChild($img);
var rect = document.querySelector(".container-wrap").getBoundingClientRect();
$img.style.left = rect.left;
$img.style.top = rect.top;
}
function dataTransfer() {
//cleanup
var $out = document.querySelector(".out-container-wrap");
while ($out.hasChildNodes()) {
$out.removeChild($out.lastChild);
}
//get source
var source = document.querySelector('.container-wrap')
//get data
var data = getSource(source);
//add data to target
setSource($out, data);
}
/**
* Get data from source div
*/
function getSource(source) {
var images = source.querySelectorAll('img');
var text = source.querySelector('div').textContent;
//build the js object and return it.
var data = {};
data.text = text;
data.image = [];
for (var i = 0; i < images.length; i++) {
var img = {}
img.url = images[i].src
img.x = images[i].dataset.x;
img.y = images[i].dataset.y;
img.h = images[i].height;
img.w = images[i].width;
data.image.push(img)
}
return data;
}
function setSource(target, data) {
//set the images.
for (var i = 0; i < data.image.length; i++) {
var d = data.image[i];
//build a new image
var $img = document.createElement('img');
$img.src = d.url;
$img.setAttribute('class', 'resize-drag');
$img.width = d.w;
$img.height = d.h;
$img.dataset.x = d.x;
$img.dataset.y = d.y;
var rect = target.getBoundingClientRect();
$img.style.left = parseInt(rect.left);
$img.style.top = parseInt(rect.top);
//transform: translate(82px, 52px)
$img.style.webkitTransform = $img.style.transform = 'translate(' + $img.dataset.x + 'px,' + $img.dataset.y + 'px)';
//$img.style.setProperty('-webkit-transform', 'translate('+$img.dataset.x+'px,'+$img.dataset.y+'px)');
target.appendChild($img);
}
//make a fresh div with text content
var $outContent = document.createElement('div')
$outContent.setAttribute('class', 'out-container-content');
$outContent.setAttribute('contenteditable', 'true');
$outContent.textContent = data.text;
target.appendChild($outContent);
}
interact('.resize-drag')
.draggable({
onmove: dragMoveListener,
inertia: true,
restrict: {
restriction: "parent",
endOnly: true,
elementRect: {
top: 0,
left: 0,
bottom: 1,
right: 1
}
}
})
.resizable({
edges: {
left: true,
right: true,
bottom: true,
top: true
},
onmove: resizeMoveListener
})
.resize-drag {
z-index: 200;
position: absolute;
border: 2px dashed #ccc;
}
.out-container-content,
.container-content {
background-color: #fafcaa;
height: 340px;
}
#btnInsertImage {
margin-bottom: 10px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="http://code.interactjs.io/interact-1.2.4.min.js"></script>
</head>
<body>
<button id="btnInsertImage" onclick="insertImage()">Insert Image</button>
<div class="container-wrap">
<div class="container-content" contenteditable="true">This is the content of the container. The content is provided along with the image. The image will be moved around and resized as required. The image can move within the boundary of the container.</div>
</div>
<button id="btnSubmit" onclick="dataTransfer()">Submit</button>
<div class="out-container-wrap">
</div>
</body>
</html>
Source
This is what I tried and it seems to be working - at least on my system, the snippet I made does not work unfortunately.
function getSelImg(){
var curObj;
window.document.execCommand('CreateLink',false, 'http://imageselectionhack/');
allLinks = window.document.getElementsByTagName('A');
for(i = 0; i < allLinks.length; i++)
{
if (allLinks[i].href == 'http://imageselectionhack/')
{
curObj = allLinks[i].firstChild;
window.document.execCommand('Undo'); // undo link
break;
}
}
if ((curObj) && (curObj.tagName=='IMG'))
{
//do what you want to...
curObj.style.border = "thick solid #0000FF";
}
}
<!DOCTYPE html>
<html>
<body>
<input type="button" onclick="getSelImg()" value="set img border">
<div contenteditable="true">This is a div.<br>
It is editable.<br>
Try to paste an image here:<br>
###########################<br>
<br>
<br>
<br>
###########################
</div>
</body>
</html>

Overlapping range inputs. On click change input with closest value

I have two overlapping range inputs, this creates a multi range input effect.
I want it so that whenever a click is made on either of these, the input with the closest value to the newly clicked value, is changed. Not entirely sure how to go about this.
How could I do this?
(function() {
"use strict";
var supportsMultiple = self.HTMLInputElement && "valueLow" in HTMLInputElement.prototype;
var descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value");
self.multirange = function(input) {
if (supportsMultiple || input.classList.contains("multirange")) {
return;
}
var values = input.getAttribute("value").split(",");
var max = +input.max || 100;
var ghost = input.cloneNode();
input.classList.add("multirange", "original");
ghost.classList.add("multirange", "ghost");
input.value = values[0] || max / 2;
ghost.value = values[1] || max / 2;
input.parentNode.insertBefore(ghost, input.nextSibling);
Object.defineProperty(input, "originalValue", descriptor.get ? descriptor : {
// Dang you Safari >:(
get: function() {
return this.value;
},
set: function(v) {
this.value = v;
}
});
Object.defineProperties(input, {
valueLow: {
get: function() {
return Math.min(this.originalValue, ghost.value);
},
set: function(v) {
this.originalValue = v;
},
enumerable: true
},
valueHigh: {
get: function() {
return Math.max(this.originalValue, ghost.value);
},
set: function(v) {
ghost.value = v;
},
enumerable: true
}
});
if (descriptor.get) {
// Again, fuck you Safari
Object.defineProperty(input, "value", {
get: function() {
return this.valueLow + "," + this.valueHigh;
},
set: function(v) {
var values = v.split(",");
this.valueLow = values[0];
this.valueHigh = values[1];
},
enumerable: true
});
}
function update() {
ghost.style.setProperty("--low", input.valueLow * 100 / max + 1 + "%");
ghost.style.setProperty("--high", input.valueHigh * 100 / max - 1 + "%");
}
input.addEventListener("input", update);
ghost.addEventListener("input", update);
update();
}
multirange.init = function() {
Array.from(document.querySelectorAll("input[type=range][multiple]:not(.multirange)")).forEach(multirange);
}
if (document.readyState == "loading") {
document.addEventListener("DOMContentLoaded", multirange.init);
} else {
multirange.init();
}
})();
#supports (--css: variables) {
input[type="range"].multirange {
-webkit-appearance: none;
padding: 0;
margin: 0;
display: inline-block;
vertical-align: top;
width: 250px;
margin-top: 50px;
margin-left: 50px;
background: lightblue;
}
input[type="range"].multirange.original {
position: absolute;
}
input[type="range"].multirange.original::-webkit-slider-thumb {
position: relative;
z-index: 2;
}
input[type="range"].multirange.original::-moz-range-thumb {
transform: scale(1);
/* FF doesn't apply position it seems */
G z-index: 1;
}
input[type="range"].multirange::-moz-range-track {
border-color: transparent;
/* needed to switch FF to "styleable" control */
}
input[type="range"].multirange.ghost {
position: relative;
background: var(--track-background);
--track-background: linear-gradient(to right, transparent var(--low), var(--range-color) 0, var(--range-color) var(--high), transparent 0) no-repeat 0 45% / 100% 40%;
--range-color: hsl(190, 80%, 40%);
}
input[type="range"].multirange.ghost::-webkit-slider-runnable-track {
background: var(--track-background);
}
input[type="range"].multirange.ghost::-moz-range-track {
background: var(--track-background);
}
}
<input type="range" multiple value="10,80" />
You'll have to capture a mouse event on the element and calculate how close it is to the high marker vs. the low marker and decide which one to update based on that. Also, because these are two stacked input elements, you'll probably have to pass the event to the low range input manually.
Here's my go at creating such a function:
function passClick(evt) {
// Are the ghost and input elements inverted? (ghost is lower range)
var isInverted = input.valueLow == ghost.value;
// Find the horizontal position that was clicked (as a percentage of the element's width)
var clickPoint = evt.offsetX / this.offsetWidth;
// Map the percentage to a value in the range (note, assumes a min value of 0)
var clickValue = max * clickPoint;
// Get the distance to both high and low values in the range
var highDiff = Math.abs(input.valueHigh - clickValue);
var lowDiff = Math.abs(input.valueLow - clickValue);
if (lowDiff < highDiff && !isInverted || (isInverted && lowDiff > highDiff)) {
// The low value is closer to the click point than the high value
// We should update the low value input
var passEvent = new MouseEvent("mousedown", {screenX: evt.screenX, clientX: evt.clientX});
// Pass a new event to the low "input" element (which is obscured by the
// higher "ghost" element, and doesn't get mouse events outside the drag handle
input.dispatchEvent(passEvent);
// The higher "ghost" element should not respond to this event
evt.preventDefault();
return false;
}
else {
console.log("move ghost");
// The high value is closer to the click point than the low value
// The default behavior is appropriate, so do nuthin
}
}
ghost.addEventListener("mousedown", passClick);
I put this code immediately above the input.addEventListener("input", update); line in your sample, and it seems to work. See my fiddle.
Some provisos though:
I only tested in Chrome. IE might have some trouble based on how I replicated the event. It may use a mechanism other than dispatchEvent... like fireEvent or something.
Initially I coded it assuming that the "ghost" element always kept track of the high range. I've since updated things to invert the event dispatching when the ghost element has the lower value--but I sped through it.
Here's something simple you could use. Although you might want to customize the style.
I am altering the z-index of the slider element based upon its proximity to the cursor.
JSFiddle
HTML
<input id='a' type='range' />
<input id='b' type='range' />
<label role='info'></label>
JS
var a = document.getElementById('a');
var b = document.getElementById('b');
a.onmousemove = function(e) {
MouseMove.call(a, e);
};
b.onmousemove = function(e) {
MouseMove.call(b, e);
};
var MouseMove = function(eventArg)
{
var max = parseInt(a.max),
min = parseInt(a.min),
diff = max - min,
clickPoint = eventArg.offsetX / a.offsetWidth,
clickPointVal = parseInt(diff * clickPoint) + min;
/* absolute distance from respective slider values */
var da = Math.abs(a.value - clickPointVal),
db = Math.abs(b.value - clickPointVal);
// Making the two sliders appear above one another only when no mouse button is pressed, this condition may be removed at will
if (!eventArg.buttons)
{
if (da < db)
{
a.style.zIndex = 2;
b.style.zIndex = 1;
}
else if (db < da)
{
b.style.zIndex = 2;
a.style.zIndex = 1;
}
}
document.querySelector('label').innerHTML = 'Red: ' + a.value + ', Green: ' + b.value + ', X: ' + eventArg.clientX;
}
CSS
input {
margin: 0px;
position: absolute;
left: 0px;
}
label {
display: inline-block;
margin-top: 100px;
}
#a {
z-index: 2;
}
#b {
z-index: 1;
}

Using Mouse Drag To Control Background Position

I want to use the 'mouse's drag' to drag a background's position around, inside a box.
The CSS:
.filmmakers #map {
width : 920px;
height : 500px;
margin-top : 50px;
margin-left : 38px;
border : 1px solid rgb(0, 0, 0);
cursor : move;
overflow : hidden;
background-image : url('WorldMap.png');
background-repeat : no-repeat;
}
The html:
<div id = "map" src = "WorldMap.png" onmousedown = "MouseMove(this)"> </div>
The Javascript:
function MouseMove (e) {
var x = e.clientX;
var y = e.clientY;
e.style.backgroundPositionX = x + 'px';
e.style.backgroundPositionY = y + 'px';
e.style.cursor = "move";
}
Nothing happens, no errors, no warnings, nothing... I have tried lots of things: an absolutely positioned image inside a div (you can guess why that didn't work), A draggable div inside a div with a background image, a table with drag and drop, and finally I tried this:
function MouseMove () {
e.style.backgroundPositionX = 10 + 'px';
e.style.backgroundPositionY = 10 + 'px';
e.style.cursor = "move";
}
This works, but its not relative to the mouse's position, pageX and pageY don't work either.
A live demo: http://jsfiddle.net/VRvUB/224/
P.S: whatever your idea is, please don't write it in JQuery
From your question I understood you needed help implementing the actual "dragging" behavior. I guess not. Anyway, here's the results of my efforts: http://jsfiddle.net/joplomacedo/VRvUB/236/
The drag only happens when the mouse button, and.. well, it behaves as I think you might want it to. Just see the fiddle if you haven't =)
Here's the code for those who want to see it here:
var AttachDragTo = (function () {
var _AttachDragTo = function (el) {
this.el = el;
this.mouse_is_down = false;
this.init();
};
_AttachDragTo.prototype = {
onMousemove: function (e) {
if ( !this.mouse_is_down ) return;
var tg = e.target,
x = e.clientX,
y = e.clientY;
tg.style.backgroundPositionX = x - this.origin_x + this.origin_bg_pos_x + 'px';
tg.style.backgroundPositionY = y - this.origin_y + this.origin_bg_pos_y + 'px';
},
onMousedown: function(e) {
this.mouse_is_down = true;
this.origin_x = e.clientX;
this.origin_y = e.clientY;
},
onMouseup: function(e) {
var tg = e.target,
styles = getComputedStyle(tg);
this.mouse_is_down = false;
this.origin_bg_pos_x = parseInt(styles.getPropertyValue('background-position-x'), 10);
this.origin_bg_pos_y = parseInt(styles.getPropertyValue('background-position-y'), 10);
},
init: function () {
var styles = getComputedStyle(this.el);
this.origin_bg_pos_x = parseInt(styles.getPropertyValue('background-position-x'), 10);
this.origin_bg_pos_y = parseInt(styles.getPropertyValue('background-position-y'), 10);
//attach events
this.el.addEventListener('mousedown', this.onMousedown.bind(this), false);
this.el.addEventListener('mouseup', this.onMouseup.bind(this), false);
this.el.addEventListener('mousemove', this.onMousemove.bind(this), false);
}
};
return function ( el ) {
new _AttachDragTo(el);
};
})();
/*** IMPLEMENTATION ***/
//1. Get your element.
var map = document.getElementById('map');
//2. Attach the drag.
AttachDragTo(map);
​
This isn't working because you are passing the element "map" to your MouseMove function, and using it as both an event object and an element. You can fix this painlessly by using JavaScript to assign your event handler rather than HTML attributes:
<div id="map"></div>
And in your JavaScript:
document.getElementById('map').onmousemove = function (e) {
// the first parameter (e) is automatically assigned an event object
var x = e.clientX;
var y = e.clientY;
// The context of this is the "map" element
this.style.backgroundPositionX = x + 'px';
this.style.backgroundPositionY = y + 'px';
}
http://jsfiddle.net/VRvUB/229/
The downside of this approach is that the backgroundPositionX and backgroundPositionY style properties are not supported in all browsers.
You mention "an absolutely positioned image inside a div" which is probably the more compatible solution for this. To make this setup work, you need to set the position of the outer element to relative, which makes absolute child elements use its bounds as zero.
<div id="map">
<img src="" alt="">
</div>
CSS:
#map {
position:relative;
overflow:hidden;
}
#map img {
position:absolute;
left:0;
top:0;
}
Here it is applied to your code: http://jsfiddle.net/VRvUB/232/
This works 100%
Vanilla Javascript
document.getElementById('image').onmousemove = function (e) {
var x = e.clientX;
var y = e.clientY;
this.style.backgroundPositionX = -x + 'px';
this.style.backgroundPositionY = -y + 'px';
this.style.backgroundImage = 'url(https://i.ibb.co/vhL5kH2/image-14.png)';
}
document.getElementById('image').onmouseleave = function (e) {
this.style.backgroundPositionX = 0 + 'px';
this.style.backgroundPositionY = 0 + 'px';
this.style.backgroundImage = 'url(https://i.ibb.co/Ph9MCB2/template.png)';
}
.container {
max-width: 670px;
height: 377px;
}
#image {
max-width: 670px;
height: 377px;
cursor: crosshair;
overflow: hidden;
background-image: url('https://i.ibb.co/Ph9MCB2/template.png');
background-repeat: no-repeat;
}
<div class="container">
<div id="image">
</div>
</div>

Javascript plugin only firing once

I've built a very basic jQuery plugin that essentially positions a sprite image left by a set amount every x milliseconds to create an animation effect. The plugin at this stage is very basic and only has a few options, and it works pretty well.
Apart from that fact that it only fires once! I have multiple instance of the animation on one page and they all fire, but only ever once each.
Now I'm not expert on Javascript and only just managed to cobble this together but here's the code anyhow:
// Animation Plugin
(function($){
$.fn.anime = function(customOptions) {
// Default Options
var defaultOptions = {
slideWidth : 100,
frames : 10,
speed : 40,
minCycles : 1,
stopFrame : 0
};
// Set options to default
var options = defaultOptions;
// Merge custom options with defaults using the setOptions func
setOptions(customOptions);
// Merge current options with the custom option
function setOptions(customOptions) {
options = $.extend(options, customOptions);
};
return this.each(function() {
// Initialize the animation object
var $elem = $('img', this);
var frameCount = 0;
var currentSlideWidth = options.slideWidth;
var intervalID = null;
var cycleCount = 1;
var animate = function() {
if (frameCount < (options.frames -1)) {
$elem.css('left', '-' + currentSlideWidth + 'px');
frameCount++;
currentSlideWidth += options.slideWidth;
}
else {
if (cycleCount < options.minCycles)
{
frameCount = 0;
currentSlideWidth = options.slideWidth;
$elem.css('left', '-' + currentSlideWidth + 'px');
cycleCount++;
}
else
{
window.clearInterval(intervalID);
$elem.css('left', '0px');
}
}
cycleCount = 1;
};
$(this).bind('mouseover', function(){
var intervalID = window.setInterval(animate, options.speed);
});
});
};
})(jQuery);
The code used to call the actual plugin on a dom element is:
$('#animeBolt').anime({
frames: 50,
slideWidth: 62,
minCycles: 1,
speed: 30,
});
This is the html it is called on:
<div class="anime" id="animeBolt">
<img src="_assets/img/anime-bolt.png" alt="Lightning Bolt" width="3100" height="114">
</div>
And finally the css:
.anime {
position: absolute;
overflow: hidden; }
.anime img {
position: absolute;
left: 0;
top: 0; }
#animeBolt {
top: 2669px;
left: 634px;
width: 62px;
height: 116px; }
How do I get the plugin to fire repeatedly?
I've created and modified jsfiddle example using your code. It's working http://jsfiddle.net/9Yz9j/16/
I've change a couple of things:
added clearInterval on mouseover to preven multiple overlapping intervals
moved intervalID variable to outside of each function, and removed var keyword from mousover handler so the script will remember intervalID set on last mouseover
reseting the frameCount, cycleCount and currentSlideWidth variables on animation end (that was actually a clue thing to get animation going more than just once)
Hope that helps

Categories