I am attempting to use React to build a website with a Google map that redraws the Polylines after I change the state. I am using setState() to set the state correctly, and I can see the values change when I print the object from this.state in the console but my Polylines on map do not re-render, and change color.
This is the main component that I am using for the project it includes a map and a table, renderMap() is what actually generates the map and update_congestion_lines is what changes the state and I am hoping to trigger a re-render.
import React, { Component } from 'react';
import {Map, Marker, GoogleApiWrapper, InfoWindow, Polyline} from 'google-maps-react';
class HomepageMap extends Component{
static defaultProps = {
center: {
lat: 33.980530,
lng: -117.377020
},
zoom: 11,
style: {
width: '100%',
height: '100%'
}
};
constructor(props){
super(props);
this.state = {
error: null,
cctvs: [],
cctv_objects: [],
center: {
lat: 33.980530,
lng: -117.377020
},
style: {
width: '100%',
height: '100%'
},
zoom: 11,
showingInfoWindow : false,
selectedPlace: {},
activeMarker: {},
image_path : [],
};
this.update_congestion_lines = this.update_congestion_lines.bind(this);
this.grabColor = this.grabColor.bind(this);
this.handleClick = this.handleClick.bind(this);
}
componentDidMount() {
var url = "http://highwayanalytics.us/api/cctv?format=json&county=Riverside,San+Bernardino";
fetch(url)
.then(res => res.json())
.then(
(result) => {
var list = [];
for(var i = 0; i < result.length; i++){
var cctv = result[i];
if(cctv.image_url !== "Not Reported"){
list.push(cctv);
}
}
this.setState({
cctvs: list,
error: false
});
},
(error) => {
//console.log(error);
this.setState({
cctvs: [],
error: true
})
});
url = "http://highwayanalytics.us/api/graph?format=json&county=Riverside,San+Bernardino";
fetch(url)
.then(res => res.json())
.then(
(result) => {
var list = [];
for(var key in result){
if(result.hasOwnProperty(key)){
var val = result[key];
var i = 0;
if(key === "SR-60" || key === "I-10" || key === "SR-91" || key === "I-210"){
for(i=0;i < val.length;i++){
var prev_cctv = null;
if(i!== 0){
prev_cctv=val[i-1];
}
var next_cctv = null;
if(i !== (val.length-1)){
next_cctv = val[i+1];
//Calc distance
var prev_lat_midpoint = null;
var prev_long_midpoint = null;
var next_lat_midpoint = null;
var next_long_midpoint = null;
var temp = null;
if(prev_cctv !== null){
if(prev_cctv.latitude > val[i].latitude){
temp = Math.abs(prev_cctv.latitude-val[i].latitude)/2;
prev_lat_midpoint = val[i].latitude + temp;
}
else{
temp = Math.abs(prev_cctv.latitude-val[i].latitude)/2;
prev_lat_midpoint = val[i].latitude - temp;
}
if(prev_cctv.longitude > val[i].longitude){
temp = Math.abs(prev_cctv.longitude-val[i].longitude)/2;
prev_long_midpoint = val[i].longitude + temp;
}
else{
temp = Math.abs(prev_cctv.longitude-val[i].longitude)/2;
prev_long_midpoint = val[i].longitude - temp;
}
}
if(next_cctv !== null){
if(next_cctv.latitude > val[i].latitude){
temp = Math.abs(next_cctv.latitude-val[i].latitude)/2;
next_lat_midpoint = val[i].latitude + temp;
}
else{
temp = Math.abs(next_cctv.latitude-val[i].latitude)/2;
next_lat_midpoint = val[i].latitude - temp;
}
if(next_cctv.longitude > val[i].longitude){
temp = Math.abs(next_cctv.longitude-val[i].longitude)/2;
next_long_midpoint = val[i].longitude + temp;
}
else{
temp = Math.abs(next_cctv.longitude-val[i].longitude)/2;
next_long_midpoint = val[i].longitude - temp;
}
}
var object = {
"cctv": val[i],
"cctv_id":val[i].cctv_id,
"prev_cctv": prev_cctv,
"next_cctv": next_cctv,
"prev_lat_midpoint": prev_lat_midpoint,
"prev_long_midpoint": prev_long_midpoint,
"next_lat_midpoint": next_lat_midpoint,
"next_long_midpoint": next_long_midpoint,
"car_count": null
}
list.push(object);
}
}
}
else if( key === "I-15" || key === "I-215"){
for(i=0;i < val.length;i++){
var prev_cctv = null;https://reactjs.org/docs/state-and-lifecycle.html
if(i!== 0){
prev_cctv=val[i-1];
}
var next_cctv = null;
if(i !== (val.length-1)){
next_cctv = val[i+1];
//Calc distance
var prev_lat_midpoint = null;
var prev_long_midpoint = null;
var next_lat_midpoint = null;
var next_long_midpoint = null;
var temp = null;
if(prev_cctv !== null){
if(prev_cctv.latitude > val[i].latitude){
temp = Math.abs(prev_cctv.latitude-val[i].latitude)/2;
prev_lat_midpoint = val[i].latitude + temp;
}
else{
temp = Math.abs(prev_cctv.latitude-val[i].latitude)/2;
prev_lat_midpoint = val[i].latitude - temp;
}
if(prev_cctv.longitude > val[i].longitude){
temp = Math.abs(prev_cctv.longitude-val[i].longitude)/2;
prev_long_midpoint = val[i].longitude + temp;
}
else{
temp = Math.abs(prev_cctv.longitude-val[i].longitude)/2;
prev_long_midpoint = val[i].longitude - temp;
}
}
if(next_cctv !== null){
if(next_cctv.latitude > val[i].latitude){
temp = Math.abs(next_cctv.latitude-val[i].latitude)/2;
next_lat_midpoint = val[i].latitude + temp;
}
else{
temp = Math.abs(next_cctv.latitude-val[i].latitude)/2;
next_lat_midpoint = val[i].latitude - temp;
}
if(next_cctv.longitude > val[i].longitude){
temp = Math.abs(next_cctv.longitude-val[i].longitude)/2;
next_long_midpoint = val[i].longitude + temp;
}
else{
temp = Math.abs(next_cctv.longitude-val[i].longitude)/2;
next_long_midpoint = val[i].longitude - temp;
}
}
object = {
"cctv": val[i],
"cctv_id": val[i].cctv_id,
"prev_cctv": prev_cctv,
"next_cctv": next_cctv,
"prev_lat_midpoint": prev_lat_midpoint,
"prev_long_midpoint": prev_long_midpoint,
"next_lat_midpoint": next_lat_midpoint,
"next_long_midpoint": next_long_midpoint,
"car_count": null
}
list.push(object);
}
}
}
else{
continue;
}
}
}
this.setState({
cctv_objects: list,
error: false
});
},
(error) => {
//console.log(error);
this.setState({
cctvs: [],
error: true
})
});
//Used for updating congestion lines, every 10 seconds
// this.intervalID = setInterval(
// ()=> this.update_congestion_lines(),
// 10000
// );
}
componentWillUnmount(){
clearInterval(this.intervalID);
}
update_congestion_lines(){
//Update Congestions Lines
console.log("Updating Congestion lines");
var cctv_objects_dup = Array.from(this.state.cctv_objects);
//Go through all cameras
for(var i = 0; i < cctv_objects_dup.length;i++){
var target_url = "http://highwayanalytics.us/api/vehicle/?cctv="+cctv_objects_dup[i].cctv_id+"&format=json";
var target_photo_id = null;
if(cctv_objects_dup[i].cctv_id !== undefined){
fetch(target_url)
.then(res => res.json())
.then(
(result) => {
if(result !== undefined){
if(result.results !== undefined){
if(result.results[0] !== undefined){
//Find most recent photo id on specific camera
target_photo_id = result.results[0].photo;
target_url = "http://highwayanalytics.us/api/vehicle/?photo="+target_photo_id+"&format=json";
fetch(target_url)
.then( res => res.json())
.then(
(result) => {
//assign that camera the result of photo count
for(let index = 0; index < cctv_objects_dup.length; index++){
if(cctv_objects_dup[index] === undefined){
console.log("undefined");
continue;
}
if(cctv_objects_dup[index].cctv_id === result.results[0].cctv){
cctv_objects_dup[index].car_count = result.count;
break;
}
}
},
(error) =>{
console.log("Error with using target_photo_id");
}
);
}
}
}
},
(error) => {
console.log("Error updating Congestion");
}
);
}
}
//update cctv objects with new car counts
this.setState({
cctv_objects: cctv_objects_dup
});
//console.log("Lines Done Updating",this.state.cctv_objects[0].car_count);
//console.log("here is cctv_objects",this.state.cctv_objects);
}
onMarkerClick = (props, marker) => {
var latest_image = "http://highwayanalytics.us/api/search/?search=" + props.name;
var path = [];
fetch(latest_image)
.then(res => res.json())
.then(
(result) => {
//console.log(result.results[0].file_name)
path.push(result.results[0].file_name)
this.setState({
activeMarker: marker,
selectedPlace: props,
showingInfoWindow: true,
image_path : path,
});
}
)
};
onMouseoverMarker= (props, marker, e) => {
// this.setState({
// activeMarker: marker,
// selectedPlace: props,
// showingInfoWindow: true
// })
// console.log(this.state.showingInfoWindow)
};
onMouseoutMarker= (props, marker, e) => {
// this.setState({
// activeMarker: null,
// showingInfoWindow: false
// })
// console.log(this.state.showingInfoWindow)
};
onInfoWindowClose = () =>
this.setState({
activeMarker: null,
showingInfoWindow: false
});
onMapClicked = () => {
if (this.state.showingInfoWindow){
this.setState({
activeMarker: null,
showingInfoWindow: false,
image_path: null,
})
console.log(this.state.showingInfoWindow)
}
};
grabColor = (car_count) =>{
console.log("Car Count", car_count);
if(car_count === null){
return 'green';
}
else{
return 'red';
}
}
renderMap(){
var icon_image = process.env.PUBLIC_URL + '/camera_icon2.png';
var cctvs = this.state.cctvs.map(
(d) =>
<Marker
icon={icon_image}
name = {d.id}
onClick = {this.onMarkerClick}
onMouseover={this.onMouseoverMarker}
onMouseout={this.onMouseoutMarker}
position = { {lat: d.latitude, lng: d.longitude} }
lat = {d.latitude}
long = {d.longitude}
image_url = {d.image_url}
route = {d.route}
/>
);
var prev_congestion_lines = this.state.cctv_objects.map(
(object)=>(
//prev_polyline
<Polyline
key= {object.cctv.latitude.toString() + object.cctv.longitude.toString()}
path={[
{ lat: object.prev_lat_midpoint, lng: object.prev_long_midpoint},
{ lat: object.cctv.latitude, lng: object.cctv.longitude},
]}
options={{
strokeColor: this.grabColor(object.car_count),
strokeOpacity: 0.75,
strokeWeight: 10,
icons: [{
offset: '0',
repeat: '10px'}],
}}
/>
)
);
var next_congestion_lines = this.state.cctv_objects.map(
(object)=>(
//prev_polyline
<Polyline
key = {object.cctv_id.toString() + object.cctv.latitude.toString() + object.cctv.longitude.toString()}
path={[
{ lat: object.cctv.latitude, lng: object.cctv.longitude},
{ lat: object.next_lat_midpoint, lng: object.next_long_midpoint},
]}
options={{
strokeColor: this.grabColor(object.car_count),
strokeOpacity: 0.75,
strokeWeight: 10,
icons: [{
offset: '0',
repeat: '10px'}],
}}
/>
)
);
return (
<div style={{ height: '92vh', width: '100%' }}>
<Map
google={this.props.google}
zoom={this.props.zoom}
style={this.props.style}
initialCenter={this.props.center}
onClick={this.onMapClicked}
>
{cctvs}
{prev_congestion_lines}
{next_congestion_lines}
</Map>
</div>
);
}
renderTable(){
var content = []
var path = "http://highwayanalytics.us/image/" + this.state.image_path;
if (this.state.showingInfoWindow !== false) {
content.push(
<div className="row" style={{'padding': '35px'}}>
<h2> Marker Information</h2>
<img
src = {path}
style={{
width: '380px'
}}
/>
<div>
<span> Lat : {this.state.selectedPlace.lat} Long: {this.state.selectedPlace.long}</span>
<p> Route : {this.state.selectedPlace.route} Marker_Id : {this.state.selectedPlace.name}</p>
</div>
</div>
);
}else{
content.push(
<div className="row" style={{'padding': '35px'}}>
<h2> Marker Information</h2>
</div>
);
}
return content;
}
handleClick(){
this.update_congestion_lines();
console.log(this.state.cctv_objects);
this.forceUpdate();
}
render(){
var map = this.renderMap();
var table = this.renderTable();
return(
<div className="row">
<div className="col-9">
{map}
</div>
<div className="col-3">
{table}
<button onClick={this.handleClick}> Update Congestion Lines</button>
</div>
</div>
);
}
}
export default GoogleApiWrapper({
apiKey: process.env.REACT_APP_GOOGLE_MAP_KEY
})(HomepageMap);
renderMap() takes care of generating the map, markers, and the polylines. The color of the polyline should change based on the car_count. As you can see am logging the car count,the car count should be null initially but is not null after the state is set, even though console.log tells me the car_count is not null, the color of the line does not change.
grabColor = (car_count) =>{
console.log("Car Count", car_count);
if(car_count === null){
return 'green';
}
else{
return 'red';
}
}
renderMap(){
var icon_image = process.env.PUBLIC_URL + '/camera_icon2.png';
var cctvs = this.state.cctvs.map(
(d) =>
<Marker
icon={icon_image}
name = {d.id}
onClick = {this.onMarkerClick}
onMouseover={this.onMouseoverMarker}
onMouseout={this.onMouseoutMarker}
position = { {lat: d.latitude, lng: d.longitude} }
lat = {d.latitude}
long = {d.longitude}
image_url = {d.image_url}
route = {d.route}
/>
);
var prev_congestion_lines = this.state.cctv_objects.map(
(object)=>(
//prev_polyline
<Polyline
key= {object.cctv.latitude.toString() + object.cctv.longitude.toString()}
path={[
{ lat: object.prev_lat_midpoint, lng: object.prev_long_midpoint},
{ lat: object.cctv.latitude, lng: object.cctv.longitude},
]}
options={{
strokeColor: this.grabColor(object.car_count),
strokeOpacity: 0.75,
strokeWeight: 10,
icons: [{
offset: '0',
repeat: '10px'}],
}}
/>
)
);
var next_congestion_lines = this.state.cctv_objects.map(
(object)=>(
<Polyline
key = {object.cctv_id.toString() + object.cctv.latitude.toString() + object.cctv.longitude.toString()}
path={[
{ lat: object.cctv.latitude, lng: object.cctv.longitude},
{ lat: object.next_lat_midpoint, lng: object.next_long_midpoint},
]}
options={{
strokeColor: this.grabColor(object.car_count),
strokeOpacity: 0.75,
strokeWeight: 10,
icons: [{
offset: '0',
repeat: '10px'}],
}}
/>
)
);
Here is what I get on the console and what the map looks like.
If anyone has an explanation of why the polylines are not being re-rendered after the car_count changes, I would be extremely grateful.
Edit: I have added to rest of the function of the first code block, and added a key to the next_congestion line as noted in the comments.
Related
Using LeafletJS and JavaScript........................................
I'm having an issue with my map. It's getting removed when I drag my marker, is there a way to fix it?
function to drag marker and route
// marker and route dragging enable
marker.dragging.enable();
marker.on("dragend", (e) => {
console.log("Marker has been moved!!!");
var lat = e.target.getLatLng().lat;
var lng = e.target.getLatLng().lng;
var pair = {lat: lat, lng: lng};
markers.set(pair, marker);
routes = [];
const tempMarker = []
markers.forEach((v, index) => {
tempMarker.push(v.getLatLng())
})
console.log({tempMarker});
**for (let i in map._layers) {
if (map._layers[i]._path != undefined) {
map.removeControl(map._layers[i]);
try {
markers.removeLayer(map._layers[i]);
} catch (e) {
console.log(e);
}
}
}**
// loop markers and move polyline
tempMarker.forEach((v, index) => {
// const lt = marker.getLatLng();
console.log('loop', v);
if (tempMarker[index + 1]) {
const nPolyline = L.polyline(
[v, tempMarker[index + 1]], {
enableDraggableLines: true,
color: "black",
weight: 5,
opacity: 0.5,
smoothFactor: 1,
}
).addTo(map);
routes.push(nPolyline);
}
})
console.log({map, routes, markers})
});
// function to drag marker and route ends here
This area area below is where it makes my map remove. how can I fix this issue?
**
for (let i in map._layers) {
if (map._layers[i]._path != undefined) {
map.removeControl(map._layers[i]);
try {
markers.removeLayer(map._layers[i]);
} catch (e) {
console.log(e);
}
}
}
**
I'm creating a quiz that requires every answer to be answered. The problem is that you should be able to skip questions if you don't have an answer. I'm trying to set a default answer everytime I press next so when I try to skip one I don't have to answer for it to have a value. The default value I want is each time the last value of my array.
next and previous question
SetQuestion(question) {
if (this.questionNumber >= 0) {
let oldAnswerButton = document.querySelectorAll('.filter_anwser');
// Deletes old question when the next question is clicked
for (let answerButton of oldAnswerButton) {
answerButton.style.display = 'none';
}
}
this.questionNumber = question;
let q = this.quiz[question];
// Check if your at the last question so the next button will stop being displayed.
if (this.questionNumber === Quiz.length - 1) {
this.nextbtn.style.display = 'none';
this.prevbtn.style.display = 'block';
this.resultbtn.style.display = 'grid';
} else if (this.questionNumber === 0) {
this.nextbtn.style.display = 'block';
this.prevbtn.style.display = 'none';
this.resultbtn.style.display = 'none';
} else {
this.nextbtn.style.display = 'block';
this.prevbtn.style.display = 'block';
this.resultbtn.style.display = 'none';
}
// Displays Question
this.questionName.textContent = q.questionText;
this.questionName.id = "questionID";
return q;
console.log(this.getLink())
console.log(this.tmp)
}
IntoArray() {
const UrlVar = new URLSearchParams(this.getLink())
this.UrlArray = [...UrlVar.entries()].map(([key, values]) => (
{[key]: values.split(",")}
)
);
}
NextQuestion() {
// let quizUrl = this.url[this.questionNumber];
let question = this.SetQuestion(this.questionNumber + 1);
let pre = question.prefix;
let prefixEqual = pre.replace('=', '');
let UrlArr = this.UrlArray;
let UrlKeys = UrlArr.flatMap(Object.keys)
let answers = question.chosenAnswer.slice(0, -1);
this.clicked = true;
// Displays answers of the questions
for (let y = 0; y < answers.length; y++) {
let item = answers[y];
// Display answer buttons
if (UrlKeys.includes(prefixEqual)) {
console.log("exists");
let btn = document.querySelector('button[value="' + item.id + '"]');
btn.style.display = 'block';
} else {
let btn = document.createElement('button');
btn.value = item.id;
btn.classList.add("filter_anwser", pre)
btn.id = 'answerbtn';
btn.textContent = item.name;
this.button.appendChild(btn);
}
// let quizUrl = control.url[control.questionNumber];
// // console.log(this.tmp);
// if (quizUrl === undefined) {
// quizUrl.push(question.prefix[y] + '');
// }
// if (quizUrl === undefined){
// this.tmp.push('');
// }
}
this.IntoArray();
}
PrevQuestion() {
let question = this.SetQuestion(this.questionNumber - 1);
let answers = question.chosenAnswer.slice(0, -1);
// Displays answers of the questions
for (let y = 0; y < answers.length; y++) {
let item = answers[y];
// Display answer buttons
let btn = document.querySelector('button[value="' + item.id + '"]');
btn.style.display = 'block';
}
this.IntoArray();
}
Link creator:
/**
* Returns the parameters for the URL.
*
* #returns {string}
*/
getLink() {
this.tmp = [];
for (let i = 0; i < this.url.length; i++) {
// Check if question is from the same quiz part and adds a , between chosen answers and add the right prefix at the beginning
if (this.url[i].length > 0) {
this.tmp.push("" + Quiz[i].prefix + this.url[i].join(","))
// console.log(this.url)
}
if (this.url[i].length === 0) {
this.tmp.push("");
}
}
/// If answers are from different quiz parts add a & between answers.
return "" + this.tmp.join("&");
// console.log(this.url[i].prefix);
};
Answer click event
control.button.addEventListener("click", function (e) {
const tgt = e.target;
control.clicked = true;
// clear the url array if there's nothing clicked
if (control.url.length === control.questionNumber) {
control.url.push([]);
}
let quizUrl = control.url[control.questionNumber];
// Check if a button is clicked. Changes color and adds value to the url array.
if (quizUrl.indexOf(tgt.value) === -1) {
quizUrl.push(tgt.value);
e.target.style.backgroundColor = "orange";
// Check if a button is clicked again. If clicked again changes color back and deletes value in the url array.
} else {
quizUrl.splice(quizUrl.indexOf(tgt.value), 1);
e.target.style.backgroundColor = "white";
}
console.log(control.getLink());
console.log(quizUrl)
})
array:
class QuizPart {
constructor(questionText, chosenAnswer, prefix, questionDescription) {
this.questionText = questionText;
this.chosenAnswer = chosenAnswer;
this.prefix = prefix;
this.questionDescription = questionDescription;
}
}
class ChosenAnswer {
constructor(id, name) {
this.id = id;
this.name = name;
}
}
let Quiz = [
new QuizPart('Whats your size?', [
new ChosenAnswer('6595', '41'),
new ChosenAnswer('6598', '42'),
new ChosenAnswer('6601', '43'),
new ChosenAnswer('', ''),
], 'bd_shoe_size_ids=',
'The size of your shoes is very important. If you have the wrong size, they wont fit.'),
new QuizPart('What color would you like?', [
new ChosenAnswer('6053', 'Red'),
new ChosenAnswer('6044', 'Blue'),
new ChosenAnswer('6056', 'Yellow'),
new ChosenAnswer('6048', 'Green'),
new ChosenAnswer('', ''),
], 'color_ids=',
'Color isn t that important, It looks good tho.'),
new QuizPart('What brand would you like?', [
new ChosenAnswer('5805', 'Adidas'),
new ChosenAnswer('5866', 'Nike'),
new ChosenAnswer('5875', 'Puma'),
new ChosenAnswer('', ''),
], 'manufacturer_ids=',
'Brand is less important. Its just your own preference'),
]
I tried giving the array's in link creator and my eventlistener a default value and replacing it when I get and actual value from one of my buttons, but it just doesn't work. Can anybody help me?
I understand, that it might be a bit far from what you expect for an answer - but why don't you have a look at a reactive tool, like Vue? It has all the tools that you might need for such a task, and maybe more:
the whole quiz can be abstracted to a simple array of objects (the questions)
next, prev, set default answer becomes a breeze
easy to extend (with questions)
simple to update (template, features, etc.)
Vue.component('QuizQuestion', {
props: ['data', 'selected'],
computed: {
valSelected: {
get() {
return this.selected
},
set(val) {
this.$emit('update:selected', val)
}
},
},
template: `
<div>
{{ data.text }}<br />
{{ data.description }}<br />
<div class="quiz-options">
<label
v-for="val in data.options"
:key="val[0]"
>
<input
type="radio"
:name="data.text"
:value="val"
v-model="valSelected"
/>
{{ val[1] }}
</label>
</div>
</div>
`
})
new Vue({
el: "#app",
computed: {
currentQuestion() {
return this.questions[this.current]
},
hasPrev() {
return !!this.current
},
hasNext() {
return this.current < this.questions.length - 1
},
},
data() {
return {
current: 0,
questions: [{
text: 'Whats your size?',
description: 'The size of your shoes is very important. If you have the wrong size, they wont fit.',
options: [
['6595', '41'],
['6598', '42'],
['6601', '43'],
['', ''],
],
selected: null,
}, {
text: 'What color would you like?',
description: 'Color isn\'t that important, It looks good tho.',
options: [
['6053', 'Red'],
['6044', 'Blue'],
['6056', 'Yellow'],
['6048', 'Green'],
['', ''],
],
selected: null,
}, {
text: 'What brand would you like?',
description: 'Brand is less important. Its just your own preference',
options: [
['5805', 'Adidas'],
['5866', 'Nike'],
['5875', 'Puma'],
['', ''],
],
selected: null,
}, ],
}
},
methods: {
selectDefault() {
this.questions[this.current] = {
...this.questions[this.current],
selected: this.questions[this.current].options.slice(-1)[0],
}
},
getPrev() {
if (this.hasPrev) {
if (!this.currentQuestion.selected) {
this.selectDefault()
}
this.current -= 1
}
},
getNext() {
if (this.hasNext) {
if (!this.currentQuestion.selected) {
this.selectDefault()
}
this.current += 1
}
},
},
template: `
<div>
<quiz-question
:data="currentQuestion"
:selected.sync="currentQuestion.selected"
/><br />
<button v-if="hasPrev" #click="getPrev">PREV</button>
<button v-if="hasNext" #click="getNext">NEXT</button>
<button v-if="!hasNext">RESULT</button>
</div>
`
})
.quiz-options {
display: flex;
flex-direction: column;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>
EDIT
But, if frameworks/libraries are not to be used, here's a more OOP approach:
class Quiz {
constructor(questions) {
this._current = 0
this._questions = questions
}
get current() {
return this._current
}
set current(val) {
this._current = val
}
get hasNext() {
return this.current < this.questions.length - 1
}
get hasPrev() {
return !!this.current
}
get questions() {
return this._questions
}
get next() {
this.current = this.hasNext ? this.current + 1 : this.current
return this.currentQuestion
}
get prev() {
this.current = this.hasPrev ? this.current - 1 : this.current
return this.currentQuestion
}
get currentQuestion() {
return this.questions[this.current]
}
}
class Question {
constructor({
text,
description,
options,
prefix,
}) {
this.text = text
this.desc = description
this.prefix = prefix
this._options = options.map(([key, val]) => ({
id: key,
value: [key, val],
selected: false,
}))
}
get options() {
return this._options
}
set options(newOptions) {
this._options = newOptions
}
get selected() {
return this.options.find(({
selected
}) => !!selected)
}
set selected(selectedVal) {
this.options = [...this.options.map(({
value: [key, val],
selected,
...rest
}) => {
return {
...rest,
value: [key, val],
selected: key === selectedVal
}
})]
}
get lastOption() {
return this.options.slice(-1)[0]
}
setDefault() {
if (!this.selected) {
this.selected = this.lastOption.id
}
}
}
const urlParser = (quiz) => {
return quiz.questions.map(({
prefix,
selected = {
value: ['']
}
}) => {
const s = selected.value[0] ? selected.value[0] : ''
return `${prefix}${s}`
}).join('&')
}
const qArr = [{
text: 'text1',
description: 'desc1',
options: [
['1_1', '11'],
['1_2', '12'],
['1_3', '13'],
],
prefix: 'prefix_1_',
},
{
text: 'text2',
description: 'desc2',
options: [
['2_1', '21'],
['2_2', '22'],
['2_3', '23'],
],
prefix: 'prefix_2_',
},
{
text: 'text3',
description: 'desc3',
options: [
['3_1', '31'],
['3_2', '32'],
['3_3', '33'],
],
prefix: 'prefix_3_',
},
]
const getOptionsHtml = ({
text,
options
}) => {
let html = ''
options.forEach(({
id,
value,
selected
}, i) => {
html += `
<label>
<input
class="question-input"
type="radio"
name="${text}"
value="${value[0]}"
${selected ? 'checked' : ''}
/>
${value[1]}
</label>
`
})
return html
}
const getSingleQuestionHtml = (q) => {
const optionsHtml = getOptionsHtml({
text: q.text,
options: q.options
})
return `
${q.text}<br />
${q.desc}<br />
${optionsHtml}
`
}
const registerEventHandlers = ({
container,
question
}) => {
const radioBtns = document.querySelectorAll('.question-input')
radioBtns.forEach((input, i) => {
input.addEventListener('change', function(e) {
question.selected = e.target.value
})
})
}
const updateHtml = ({
container,
question
}) => {
container.innerHTML = getSingleQuestionHtml(question)
registerEventHandlers({
container,
question
})
};
const updateContainer = (container) => (question) => updateHtml({
container,
question
});
const setElDisplay = ({
el,
display
}) => {
if (display) {
el.classList.add("d-inline-block")
el.classList.remove("d-none")
} else {
el.classList.remove("d-inline-block")
el.classList.add("d-none")
}
}
const updateBtnVisibility = ({
btnNext,
btnPrev,
btnResult,
quiz
}) => () => {
setElDisplay({
el: btnNext,
display: quiz.hasNext
})
setElDisplay({
el: btnResult,
display: !quiz.hasNext
})
setElDisplay({
el: btnPrev,
display: quiz.hasPrev
})
}
(function() {
const quiz = new Quiz(qArr.map(q => new Question(q)))
const container = document.getElementById('quiz-container')
updateQuizContainer = updateContainer(container)
updateQuizContainer(quiz.currentQuestion)
const btnPrev = document.getElementById('btn-prev')
const btnNext = document.getElementById('btn-next')
const btnResult = document.getElementById('btn-result')
const updateBtns = updateBtnVisibility({
btnPrev,
btnNext,
btnResult,
quiz
})
updateBtns()
btnPrev.addEventListener('click', function() {
if (quiz.hasPrev) {
quiz.currentQuestion.setDefault()
}
updateQuizContainer(quiz.prev)
updateBtns()
})
btnNext.addEventListener('click', () => {
if (quiz.hasNext) {
quiz.currentQuestion.setDefault()
}
updateQuizContainer(quiz.next)
updateBtns()
})
btnResult.addEventListener('click', function() {
quiz.currentQuestion.setDefault()
console.log(urlParser(quiz))
})
})();
.d-inline-block {
display: inline-block;
}
.d-none {
display: none;
}
<div id="quiz-container"></div>
<div id="quiz-controls">
<button id="btn-prev" class="d-inline-block">
PREV
</button>
<button id="btn-next" class="d-inline-block">
NEXT
</button>
<button id="btn-result" class="d-none">
RESULT
</button>
</div>
<div id="result"></div>
I am creating an app with a map on the home screen. Whenever the user clicks a marker, it's supposed to open an absolute-positioned div from the bottom of the screen, with info on the place clicked. The issue is: the div's visibility and position are ruled by an ngIf (to make it visible) and an ngClass (to make it slide upwards). When the pin is clicked, google maps' event listener fires and calls a function I defined for the marker click event. This function changes state of the variables used for ngClass and ngIf. When the marker is clicked, that function runs immediately, but I can only see the bottom div appear only after I click on another element of the home screen. This behavior was verified in the chrome inspector tool, the ngIf state and the ngClass are changed just at that moment where I click in any other element on the screen. The map used is google maps' JS implementation. I used successfully the agm implementation, but I needed support for custom drawn markers and couldn't make it work with agm.
Here is the code for the home screen:
<ion-content>
<div class="places">
<ion-list has-header>
<ion-item>
<ion-label style="margin: 0px !important;">Lugares</ion-label>
<ion-select interface="popover">
<ion-option *ngFor="let place of placesCombo" [value]="palce" [selected]="place=='Perto de mim'">{{place}}</ion-option>
<!-- (ionSelect)="onTypeChange(place)" -->
</ion-select>
</ion-item>
</ion-list>
<ion-searchbar class="home-search" [(ngModel)]="searchQuery" [showCancelButton]="true" (keyup.enter)="closeKeyboard()" (ionInput)="onSearchbarInput($event)"
(ionCancel)="onCancel($event)" placeholder="Buscar um lugar"></ion-searchbar>
<ion-list class="places-list" *ngIf="showList">
<button ion-item *ngFor="let place of searchPlaces" (click)="centerPlace(place)">
{{ place.name }}
</button>
</ion-list>
</div>
<div class="map-wrap">
<div #map id="map"></div>
<div *ngIf="placeInfo != null" class="establishment-card" [ngClass]="{'active': isCardShown }">
<div class="bg" [ngStyle]="getStyle()">
</div>
<button ion-button round class="close" (click)="dismissCard(this)">
<ion-icon class="icon-close"></ion-icon>
</button>
<div card-content no-lines no-padding>
<h1 text-center>{{getName()}}</h1>
<ion-icon text-center white *ngFor="let item of starData; let i = index">
<i icon-medium *ngIf="starData[i].state == 'empty'" class="icon icon-star-outline"></i>
<i icon-medium *ngIf="starData[i].state == 'half'" class="icon icon-star-half"></i>
<i icon-medium *ngIf="starData[i].state == 'full'" class="icon icon-star"></i>
</ion-icon>
<div class="reviews" span-small text-center margin-bottom>{{getRate()}} (78 avaliações)</div>
<button ion-button block icon-start (click)="checkOffers()">
<ion-icon name="pricetags"></ion-icon>OFERTAS</button>
<button ion-button block icon-start (click)="checkDetails()">
<ion-icon name="search"></ion-icon>VER MAIS</button>
<button ion-button block icon-start (click)="navigate()">
<ion-icon name="navigate"></ion-icon>IR AGORA</button>
</div>
</div>
</div>
This screen is one of the pages in a 3-tab layout. I don't think this can an aggravating, but the more we know, the better.
This is the TS class for the home screen:
import { Component } from '#angular/core';
import { IonicPage, NavController, NavParams, AlertController, ActionSheetController, Keyboard } from 'ionic-angular';
import { Geolocation } from '#ionic-native/geolocation';
import { Http, URLSearchParams } from '#angular/http';
import { GlobalVars } from '../../../providers/global-variables';
import { Establishment } from '../../../models/establishment';
import { AlertUtils } from '../../../utils/alert-utils';
import { NavUtils } from '../../../utils/nav-utils';
import { EstablishmentDetailsPage } from '../../establishment-details/establishment-details';
import { LaunchNavigator } from '../../../../node_modules/#ionic-native/launch-navigator';
import { EstablishmentOffersPage } from '../../establishment-offers/establishment-offers';
import { Diagnostic } from '#ionic-native/diagnostic';
import { Storage } from '../../../../node_modules/#ionic/storage';
import { MapUtils } from '../../../utils/map-utils';
declare var google;
declare var CustomMarker;
#IonicPage()
#Component({
selector: 'page-home',
templateUrl: 'home.html',
})
export class HomePage {
static title = 'Home';
homeInstance: any;
gMap: any;
myPos: any = {
}
markers: any[] = [];
placesCombo: any[] = ['Perto de mim', 'Porto Alegre'];
searchQuery: string = '';
searchPlaces = [];
showList = false;
places: Array<Establishment> = [];
placeInfo: Establishment = null;
isCardShown: boolean = false;
starData = [
{
"state": 'empty',
},
{
"state": 'empty',
},
{
"state": 'empty',
},
{
"state": 'empty',
},
{
"state": 'empty',
}
];
constructor(public navCtrl: NavController
, public navParams: NavParams
, public geolocation: Geolocation
, public http: Http
, public alertCtrl: AlertController
, public nav: LaunchNavigator
, public diag: Diagnostic
, public actionSheetCtrl: ActionSheetController
, public storage: Storage
, public keyboard: Keyboard) {
this.homeInstance = navParams.data;
this.homeInstance.tabHome = this;
}
ionViewWillLoad() {
this.geolocation.getCurrentPosition().then((resp) => {
this.myPos.lat = resp.coords.latitude;
this.myPos.lng = resp.coords.longitude;
const position = new google.maps.LatLng(this.myPos.lat, this.myPos.lng);
this.gMap.setCenter(position);
var contentString = '<div id="content">' +
'Você está aqui' +
'</div>';
var infowindow = new google.maps.InfoWindow({
content: contentString
});
var overlay = new CustomMarker(
position,
this.gMap,
{
//------------Here I create the marker for the user's position. Oddly, if I click at this marker first, the other ones seem to work ok after that.
listener: function () {
infowindow.open(this.gMap, overlay);
},
image: "http://www.stickpng.com/assets/images/585e4bf3cb11b227491c339a.png"
}
);
}).catch((error) => {
console.log('Error getting location', error);
});
this.populateLists();
}
ionViewDidLoad() {
const position = new google.maps.LatLng(0, 0);
const mapOptions = {
zoom: 15,
center: position,
disableDefaultUI: true,
zoomControl: true,
scaleControl: true
}
this.gMap = new google.maps.Map(document.getElementById('map'), mapOptions);
this.gMap.setOptions({
styles: [
{
"featureType": "poi",
"elementType": "labels.icon",
"stylers": [
{
"visibility": "off"
}
]
}
]
});
}
populateLists() {
this.http.get(GlobalVars.serverRoot + 'establishments/summary'
).map(res => res.json()).toPromise().then(
data => {
this.places = new Array<Establishment>();
for (let i = 0; i < data.length; i++) {
let establishment = new Establishment(data[i]);
this.places.push(establishment);
let position = new google.maps.LatLng(establishment.location.cordinates.split(', ')[0], establishment.location.cordinates.split(', ')[1]);
const home = this;
var overlay = new CustomMarker(
position,
this.gMap,
{
listener: function () {
//----Here I send the code block I want to run after the marker is clicked.
home.onMarkerClick(establishment);
},
image: establishment.images[0].path_miniature
}
);
this.markers.push(overlay);
};
this.diag.isLocationEnabled().then((state) => {
if (!state) {
let alert = this.alertCtrl.create({
title: "Atenção",
message: "Para obter uma experiência mais personalizada, ative os serviços de localização. Deseja fazer isso agora?",
enableBackdropDismiss: false,
buttons: [{
text: 'Não', handler: (data) => {
alert.dismiss();
}
}, {
text: 'Sim', handler: (data) => {
this.diag.switchToLocationSettings();
}
}]
});
alert.present();
}
});
},
error => {
AlertUtils.showErrorWithHandler(error, this.alertCtrl, (data) => {
this.diag.isLocationEnabled().then((state) => {
if (!state) {
let alert = this.alertCtrl.create({
title: "Atenção",
message: "Para obter uma experiência mais personalizada, ative os serviços de localização. Deseja fazer isso agora?",
enableBackdropDismiss: false,
buttons: [{
text: 'Não', handler: (data) => {
alert.dismiss();
}
}, {
text: 'Sim', handler: (data) => {
this.diag.switchToLocationSettings();
}
}]
});
alert.present();
}
});
});
});
}
getLat(place: any) {
return Number(place.location.cordinates.split(', ')[0]);
}
getLng(place: any) {
return Number(place.location.cordinates.split(', ')[1]);
}
//-----------------THIS is the function called when a marker is clicked, and changes the variables that govern my bottom card behavior. It is called immediately after the click on the marker, but its effects' visibility are delayed
onMarkerClick(place) {
this.placeInfo = place;
this.onStarClass(this.placeInfo.rank);
let home = this;
setTimeout(function () {
home.isCardShown = true;
}, 100);
}
dismissCard(home: HomePage) {
this.isCardShown = false;
setTimeout(function () {
home.placeInfo = null;
}, 400);
}
onCancel($event) { }
closeKeyboard() {
this.keyboard.close();
}
onSearchbarInput(ev: any) {
// set val to the value of the searchbar
let val = ev.target.value;
this.searchQuery = val;
this.getItems(ev);
}
getItems(ev: any) {
// Reset items back to all of the items
this.searchPlaces = [];
// set val to the value of the searchbar
let val = ev.target.value;
// if the value is an empty string don't filter the items
if (val && val.trim() != '') {
let qp = new URLSearchParams();
if (this.searchQuery != null && this.searchQuery.trim() != '') {
qp.append('name', this.searchQuery);
}
this.http.get(GlobalVars.serverRoot + 'establishments/summary', { params: qp }).toPromise().then(
response => {
let places = response.json();
for (let i = 0; i < places.length; i++) {
this.searchPlaces.push(new Establishment(places[i]));
}
});
// Show the results
this.showList = true;
} else {
// hide the results when the query is empty
this.showList = false;
}
}
centerPlace(place: Establishment) {
this.searchQuery = place.name;
this.showList = false;
this.searchPlaces = [];
}
checkOffers() {
NavUtils.goToPage(EstablishmentOffersPage, { establishment: this.placeInfo });
}
checkDetails() {
NavUtils.goToPage(EstablishmentDetailsPage, { establishment: this.placeInfo });
}
navigate() {
MapUtils.navigate(this.placeInfo, this.actionSheetCtrl, this.alertCtrl);
}
getStyle() {
if (this.placeInfo != null) {
return { 'background-image': 'url(' + this.placeInfo.images[0].path + ')' };
} else return '';
}
getName() {
if (this.placeInfo != null) {
return this.placeInfo.name;
} else return '';
}
getRate() {
if (this.placeInfo != null) {
return this.placeInfo.rank;
} else return '';
}
onStarClass(rate: number) {
let decimal = parseInt(rate.toPrecision(2).slice(2));
let litStars = parseInt((rate).toPrecision(2).charAt(0)) + (decimal >= 7 ? 1 : 0);
let hasHalfLitStar = decimal >= 2 && decimal < 7;
for (let i = 1; i <= this.starData.length; i++) {
if (i <= litStars) {
this.starData[i - 1].state = 'full';
} else if (i == litStars + 1 && hasHalfLitStar) {
if (hasHalfLitStar) {
this.starData[i - 1].state = 'half'
}
}
}
};
}
This is the custom marker js file, which I found online in a great tutorial. I did edit it to make it render the markers as I wanted, and to be recognized by the TS classes.
var CustomMarker = function (latlng, map, args) {
this.latlng = latlng;
this.args = args;
this.setMap(map);
}
CustomMarker.prototype = new google.maps.OverlayView();
CustomMarker.prototype.draw = function () {
var self = this;
var div = this.div;
if (!div) {
div = this.div = document.createElement('div');
div.className = 'marker';
div.style.position = 'absolute';
div.style.cursor = 'pointer';
div.style.width = '36px';
div.style.height = '46px';
div.style.backgroundImage = "url('https://s3-sa-east-1.amazonaws.com/clubi-app/pin.png')";
div.style.backgroundPosition = "center";
div.style.backgroundSize = "36px 46px";
imgCropper = document.createElement('div');
imgCropper.style.position = 'absolute';
imgCropper.style.zIndex = '-1';
imgCropper.style.cursor = 'pointer';
imgCropper.style.width = '34px';
imgCropper.style.height = '34px';
imgCropper.style.margin = '1px';
imgCropper.style.overflow = 'hidden';
imgCropper.style.borderRadius = '50%';
imgDiv = document.createElement('div');
imgDiv.style.position = 'absolute';
imgDiv.style.cursor = 'pointer';
imgDiv.style.width = '34px';
imgDiv.style.height = '34px';
if (typeof (self.args.image) !== 'undefined') {
imgDiv.style.backgroundImage = "url('" + this.args.image + "')";
imgDiv.style.backgroundPosition = "center";
imgDiv.style.backgroundSize = "34px 34px";
}
imgCropper.appendChild(imgDiv);
div.appendChild(imgCropper);
if (typeof (self.args.marker_id) !== 'undefined') {
div.dataset.marker_id = self.args.marker_id;
}
if (typeof (self.args.listener) !== 'undefined') {
google.maps.event.addDomListener(div, "click", self.args.listener);
}
var panes = this.getPanes();
panes.overlayImage.appendChild(div);
}
var point = this.getProjection().fromLatLngToDivPixel(this.latlng);
if (point) {
div.style.left = (point.x - 18) + 'px';
div.style.top = (point.y - 46) + 'px';
}
};
CustomMarker.prototype.remove = function () {
if (this.div) {
this.div.parentNode.removeChild(this.div);
this.div = null;
}
};
CustomMarker.prototype.getPosition = function () {
return this.latlng;
};
If there's anything else I can provide to help identify the cause of this issue, please let me know.
I tried to create a simple todo app with Cyclejs/xstream. The app works fine. Only thing I not able to understand is after adding each todo the input should clear, which is not happening.
todo.js
import {
div, span, p, input, ul, li, button, body
}
from '#cycle/dom'
import xs from 'xstream'
import Utils from './utils'
export function Todo(sources) {
const sinks = {
DOM: view(model(intent(sources)))
}
return sinks
}
function intent(sources) {
return {
addTodo$: sources.DOM.select('input[type=text]').events('keydown').filter((ev) => {
return ev.which == 13 && ev.target.value.trim().length > 0;
}).map((ev) => {
return ev.target.value;
}),
deleteTodo$: sources.DOM.select('.delete').events('click').map((ev) => {
return Number(ev.target.getAttribute('data-id'));
}).filter((id) => {
return !isNaN(id);
}),
completeTodo$: sources.DOM.select('.complete').events('click').map((ev) => {
return Number(ev.target.getAttribute('data-id'));
}).filter((id) => {
return !isNaN(id);
})
};
}
function model(action$) {
let deleteTodo$ = action$.deleteTodo$.map((id) => {
return (holder) => {
let index = Utils.findIndex(holder.currentTodos, 'id', id);
if (index > -1) holder.currentTodos.splice(index, 1);
return {
currentTodos: holder.currentTodos,
value: ''
};
};
});
let completeTodo$ = action$.completeTodo$.map((id) => {
return (holder) => {
let index = Utils.findIndex(holder.currentTodos, 'id', id);
if (index > -1) holder.currentTodos[index].completed = !holder.currentTodos[index].completed;
return {
currentTodos: holder.currentTodos,
value: ''
};
};
});
let addTodo$ = action$.addTodo$.map((item) => {
return (holder) => {
let todo = {
value: item,
id: holder.currentTodos.length + 1,
completed: false
};
holder.currentTodos.push(todo);
return {
currentTodos: holder.currentTodos,
value: ''
};
};
});
return xs.merge(deleteTodo$, addTodo$, completeTodo$)
.fold((holder, modifier) => {
return modifier(holder);
}, {
currentTodos: [],
value: ''
});
}
function view(state$) {
return state$.map((state) => {
console.log(state);
return div({
attrs: {
class: 'todo'
}
}, [
input({
props: {
type: 'text',
value: state.value
}
}),
ul({
attrs: {
class: 'text'
}
}, state.currentTodos.map((todo) => {
return li({
attrs: {
class: `${todo.completed ? 'completed' : 'open'}`
}
}, [
span(todo.value),
button({
attrs: {
class: 'delete',
'data-id': todo.id
}
}, 'XXXXX'),
button({
attrs: {
class: 'complete',
'data-id': todo.id
}
}, 'CCCCC')
]);
}))
]);
});
}
utils.js
var Utils = {
filter: function(array, fn) {
var results = [];
var item;
for (var i = 0, len = array.length; i < len; i++) {
item = array[i];
if (fn(item)) results.push(item);
}
return results;
},
findItem: function(array, fn) {
for (var i = 0, len = array.length; i < len; i++) {
var item = array[i];
if (fn(item)) return item;
}
return null;
},
findIndex: function(array, prop, value) {
var pointerId = -1;
var index = -1;
var top = array.length;
var bottom = 0;
for (var i = array.length - 1; i >= 0; i--) {
index = bottom + (top - bottom >> 1);
pointerId = array[index][prop];
if (pointerId === value) {
return index;
} else if (pointerId < value) {
bottom = index;
} else if (pointerId > value) {
top = index;
}
}
return -1;
}
};
export default Utils;
You need to put hook inside input element. It will work as expected. If you want you can send another default value (in this case it is empty string).
input({
props: {
type: 'text'
},
hook: {
update: (o, n) => n.elm.value = ''
}
}),
#cycle/dom driver should be > 11.0.0 which works with Snabbdom. But if you use earlier version you need:
var Hook = function(){
this.arguments=arguments;
}
Hook.prototype.hook = function(node) {
node.value=this.arguments[0];
}
input({ attributes: {type: 'text'},
'my-hook':new Hook('')
})
I need to make a Rickshaw legend where I can make groups of time series.
e.g. I have 20 time series displayed in a graph and I want them to be grouped in 4 groups, named series1, series2, series3, series4. Series1 would contain the first 5 time series in the graph, series2 would contain the 6th to the 10th and so on.
Before I try to add a custom object in my .js to solve my problem someone knows if there is a built in functionality that I couldn't find?
I didn't find anything helpful so I created a personalized version of a multiple legend that works along with the traditional legend.
Unfortunately to update the multiple legend when the standard legend is modified I had to create a myLegend object instead of the default one.
You can use this version in this way:
//you must have a traditional Rickshaw seriesData
var graph = new Rickshaw.Graph( {
element: document.getElementById("chart"),
width: 960,
height: 500,
renderer: 'line',
series: seriesData
} );
graph.render();
var legend = new Rickshaw.Graph.Legend( {
graph: graph,
element: document.getElementById('legend')
} );
var mLegend = new multiLegend( {
graph: graph,
element: document.getElementById('multi_legend'),
multiDivision: [
{
name: "Africa",
indexes: [ 0, 1, 2, 3, 4, 5, 6, 7 ]
}
{
name: "Asia",
indexes: [ 8, 9, 10, 11, 12 ]
},
{
name: "Europe",
indexes: [ 13, 14, 15, 16, 17]
},
{
name: "North America",
indexes: [ 18, 19, 20 ]
}
]
} );
new myToggle( {
graph: graph,
legend: legend,
multiLegend: mLegend
} );
new multiToggle( {
graph: graph,
multiLegend: mLegend,
legend: legend
});
Here's the code:
multiLegend = Rickshaw.Class.create( {
className: 'rickshaw_legend',
initialize: function(args) {
this.element = args.element;
this.graph = args.graph;
this.naturalOrder = args.naturalOrder;
this.seriesGroups = args.multiDivision;
this.element.classList.add(this.className);
this.list = document.createElement('ul');
this.element.appendChild(this.list);
this.render();
// we could bind this.render.bind(this) here
// but triggering the re-render would lose the added
// behavior of the series toggle
this.graph.onUpdate( function() {} );
},
render: function() {
var self = this;
while ( this.list.firstChild ) {
this.list.removeChild( this.list.firstChild );
}
this.lines = [];
var allSeries = this.graph.series;
self.seriesGroups.forEach( function(s) {
var series = allSeries.filter( function(value, index) {
return (s.indexes.indexOf(index)!=-1) ? true : false;
});
series = series.reverse();
self.addLine(s.name, series);
} );
},
addLine: function (name, series) {
var line = document.createElement('li');
line.className = 'line';
if (series.disabled) {
line.className += ' disabled';
}
if (series.className) {
d3.select(line).classed(series.className, true);
}
var swatch = document.createElement('div');
swatch.className = 'swatch';
swatch.style.backgroundColor = "#0000FF";
line.appendChild(swatch);
var label = document.createElement('span');
label.className = 'label';
label.innerHTML = name;
line.appendChild(label);
this.list.appendChild(line);
line.series = series;
var _line = { element: line, series: series, disabled: false};
if (this.shelving) {
this.shelving.addAnchor(_line);
this.shelving.updateBehaviour();
}
if (this.highlighter) {
this.highlighter.addHighlightEvents(_line);
}
this.lines.push(_line);
return line;
}
} );
multiToggle = function(args) {
this.graph = args.graph;
this.multiLegend = args.multiLegend;
this.legend = args.legend;
var self = this;
this.addAnchor = function(line) {
var anchor = document.createElement('a');
anchor.innerHTML = '✔';
anchor.classList.add('action');
line.element.insertBefore(anchor, line.element.firstChild);
anchor.onclick = function(e) {
if (line.disabled) {
line.series.forEach( function(serie) {
serie.enable();
});
line.element.classList.remove('disabled');
line.disabled = false;
self.legend.lines
.filter(function(value) {
return (line.series.indexOf(value.series)!=-1) ? true : false;
})
.forEach( function(l) {
l.element.classList.remove('disabled');
l.disabled = false;
});
} else {
if (this.graph.series.filter(function(s) { return !s.disabled }).length <= 1) return;
line.series.forEach( function(serie) {
serie.disable();
});
line.element.classList.add('disabled');
line.disabled = true;
self.legend.lines
.filter(function(value) {
return (line.series.indexOf(value.series)!=-1) ? true : false;
})
.forEach( function(l) {
l.element.classList.add('disabled');
l.disabled = true;
});
}
self.graph.update();
}.bind(this);
var label = line.element.getElementsByTagName('span')[0];
label.onclick = function(e){
var disableAllOtherLines = line.disabled;
if ( ! disableAllOtherLines ) {
for ( var i = 0; i < self.multiLegend.lines.length; i++ ) {
var l = self.multiLegend.lines[i];
if ( line.series === l.series ) {
// noop
} else if ( l.series.disabled ) {
// noop
} else {
disableAllOtherLines = true;
break;
}
}
}
// show all or none
if ( disableAllOtherLines ) {
// these must happen first or else we try ( and probably fail ) to make a no line graph
line.series.forEach( function(serie) {
serie.enable();
});
line.element.classList.remove('disabled');
line.disabled = false;
self.legend.lines
.filter(function(value) {
return (line.series.indexOf(value.series)!=-1) ? true : false;
})
.forEach( function(l) {
l.element.classList.remove('disabled');
l.disabled = false;
});
self.multiLegend.lines.forEach(function(l){
if ( line.series === l.series ) {
// noop
} else {
l.series.forEach( function(serie) {
serie.disable();
});
l.element.classList.add('disabled');
l.disabled = true;
self.legend.lines
.filter(function(value) {
return (l.series.indexOf(value.series)!=-1) ? true : false;
})
.forEach( function(l2) {
l2.element.classList.add('disabled');
l2.disabled = true;
});
}
});
} else {
self.multiLegend.lines.forEach(function(l){
l.series.forEach( function(serie) {
serie.enable();
});
l.element.classList.remove('disabled');
l.disabled = false;
self.legend.lines
.filter(function(value) {
return (l.series.indexOf(value.series)!=-1) ? true : false;
})
.forEach( function(l2) {
l2.element.classList.remove('disabled');
l2.disabled = false;
});
});
}
self.graph.update();
};
};
if (this.multiLegend) {
var $ = jQuery;
if (typeof $ != 'undefined' && $(this.multiLegend.list).sortable) {
$(this.multiLegend.list).sortable( {
start: function(event, ui) {
ui.item.bind('no.onclick',
function(event) {
event.preventDefault();
}
);
},
stop: function(event, ui) {
setTimeout(function(){
ui.item.unbind('no.onclick');
}, 250);
}
});
}
this.multiLegend.lines.forEach( function(l) {
self.addAnchor(l);
} );
}
this._addBehavior = function() {
this.graph.series.forEach( function(s) {
s.disable = function() {
if (self.graph.series.length <= 1) {
throw('only one series left');
}
s.disabled = true;
};
s.enable = function() {
s.disabled = false;
};
} );
};
this._addBehavior();
this.updateBehaviour = function () { this._addBehavior() };
};
myToggle = function(args) {
this.graph = args.graph;
this.legend = args.legend;
this.multiLegend = args.multiLegend;
var self = this;
this.addAnchor = function(line) {
var anchor = document.createElement('a');
anchor.innerHTML = '✔';
anchor.classList.add('action');
line.element.insertBefore(anchor, line.element.firstChild);
anchor.onclick = function(e) {
if (line.series.disabled) {
line.series.enable();
line.element.classList.remove('disabled');
} else {
if (this.graph.series.filter(function(s) { return !s.disabled }).length <= 1) return;
line.series.disable();
line.element.classList.add('disabled');
self.multiLegend.lines.forEach( function(l) {
if(l.series.indexOf(line.series)!=-1) {
l.element.classList.add('disabled');
l.disabled = true;
}
});
}
self.graph.update();
}.bind(this);
var label = line.element.getElementsByTagName('span')[0];
label.onclick = function(e){
var disableAllOtherLines = line.series.disabled;
if ( ! disableAllOtherLines ) {
for ( var i = 0; i < self.legend.lines.length; i++ ) {
var l = self.legend.lines[i];
if ( line.series === l.series ) {
// noop
} else if ( l.series.disabled ) {
// noop
} else {
disableAllOtherLines = true;
break;
}
}
}
// show all or none
if ( disableAllOtherLines ) {
// these must happen first or else we try ( and probably fail ) to make a no line graph
line.series.enable();
line.element.classList.remove('disabled');
self.legend.lines.forEach(function(l){
if ( line.series === l.series ) {
// noop
} else {
l.series.disable();
l.element.classList.add('disabled');
}
});
self.multiLegend.lines.forEach( function(l) {
l.element.classList.add('disabled');
l.disabled = true;
});
} else {
self.legend.lines.forEach(function(l){
l.series.enable();
l.element.classList.remove('disabled');
});
}
self.graph.update();
};
};
if (this.legend) {
var $ = jQuery;
if (typeof $ != 'undefined' && $(this.legend.list).sortable) {
$(this.legend.list).sortable( {
start: function(event, ui) {
ui.item.bind('no.onclick',
function(event) {
event.preventDefault();
}
);
},
stop: function(event, ui) {
setTimeout(function(){
ui.item.unbind('no.onclick');
}, 250);
}
});
}
this.legend.lines.forEach( function(l) {
self.addAnchor(l);
} );
}
this._addBehavior = function() {
this.graph.series.forEach( function(s) {
s.disable = function() {
if (self.graph.series.length <= 1) {
throw('only one series left');
}
s.disabled = true;
};
s.enable = function() {
s.disabled = false;
};
} );
};
this._addBehavior();
this.updateBehaviour = function () { this._addBehavior() };
};