Vue nested data: child component not hidden when parent node is deleted - javascript

I have Vue displaying a few copies of a component according to the data source in the root instance. The data is a nested JS object with children. Pointers to parent nodes are added once the data is loaded. In the view I can drill down and up, displaying one level of the hierarchy at a time. No recursion is used in the view.
My question is: Why don't the child components disappear when their parent node is deleted?
Please ignore the lengthy CSS: it's part of larger project and not relevant to the question at hand.
Since SO snippets don't display this well, here's a CodePen: https://codepen.io/MSCAU/pen/RmWOWE. The difficulty is that it still doesn't allow you to fire up Vue DevTools and peek under the hood so I'll try and simplify further or get a standalone page up.
var source = {
"name": "root",
"value": 9,
"id": 0,
"children": [{
"name": "Nodes",
"value": 32,
"id": 100,
"children": [{
"name": "Fish",
"value": 20,
"id": 1,
"children": [{
"name": "Cod",
"value": 5,
"id": 10,
},{
"name": "Salmon",
"value": 15,
"id": 110,
}]
}, {
"name": "Drinks",
"value": 12,
"id": 3,
"children": [{
"name": "Juice",
"value": 8,
"id": 11,
},
{
"name": "Wine",
"value": 4,
"id": 12,
}]
}]
}]
};
function clone(obj) {
/* Standard clone function */
if(obj === null || typeof(obj) !== 'object' || 'isActiveClone' in obj)
return obj;
var temp = obj.constructor(); // changed
for(var key in obj) {
if(Object.prototype.hasOwnProperty.call(obj, key)) {
obj['isActiveClone'] = null;
temp[key] = clone(obj[key]);
delete obj['isActiveClone'];
}
}
return temp;
}
Vue.component('bar-wrapper', {
props: ['node', 'index', 'side'],
template: '#bar-wrapper',
methods: {
amount(node) {
// bar.value = bar.old_value / 100 * this.root;
return Math.abs(node.value / node.parent.value) * 100;
},
drillDown(side, node, event) {
this.$emit('drill-to', side, node);
}
}
});
var app = new Vue({
el: '#main',
data: {
db: {
tree: clone(source)
},
expenses: {
children: []
}
},
computed: {
expenses_view_nodes() {
return this.expenses.children;
}
},
methods: {
amount(node) {
return Math.abs(node.value / node.parent.value) * 100;
},
compare(a, b) {
if (Math.abs(a.value) < Math.abs(b.value))
return 1;
if (Math.abs(a.value) > Math.abs(b.value))
return -1;
return 0;
},
makeParents(node) {
var _this = this;
if (!node.hasOwnProperty("children"))
return false;
else {
var parent_node = node;
$.each(node.children, function() {
this.parent = parent_node;
_this.makeParents(this);
});
}
return node;
},
sortMe() {
if (this.expenses && this.expenses.children) {
this.expenses.children.sort(this.compare);
}
return false;
},
drillDown(side, node) {
this.expenses = node;
},
drillUp(side, node, event) {
if (node.parent && node.parent.id != 0) {
this.expenses = node.parent;
}
},
insertCake() {
this.db.tree.children[0].children.push({id: Math.floor(Math.random() * 10000), name: "Cake", value: 14, parent: this.db.tree.children[0]});
this.db.tree.children[0].value += 14;
},
deleteFish() {
var this_amount = this.db.tree.children[0].children[0].value;
this.db.tree.children[0].children.splice(0,1);
this.db.tree.children[0].value -= this_amount;
},
deleteChild() {
// Vue.delete(this.db.tree.children[0].children[0].children, 0);
var this_amount = this.db.tree.children[0].children[0].children[0].value;
this.db.tree.children[0].children[0].children.splice(0,1);
this.db.tree.children[0].children[0].value -= this_amount;
this.db.tree.children[0].value -= this_amount;
},
init() {
this.db.tree = clone(source);
this.expenses = this.db.tree.children[0];
this.makeParents(this.db.tree);
this.sortMe();
}
},
watch: {
db: {
handler(newVal, oldVal) {
console.log("Watched the db");
},
deep: true
}
},
beforeCreate() {
console.log("%cBefore create hook: Component is not created and data variables are not available yet", "color:green");
},
created() {
console.log("%cCreated hook: Component is created and data variables are available", "color:green");
this.init();
},
beforeMount() {
console.log("%cBefore mount hook: Component is not mounted on DOM yet", "color:green");
},
mounted() {
console.log("%cMounted hook: Component is mounted on DOM", "color:green");
},
beforeUpdate() {
console.log("%cBefore update hook: Component is not updated yet", "color:green");
},
updated() {
console.log("%cUpdated hook: Component is updated", "color:green");
},
beforeDestroy() {
console.log("%cBefore destroy hook: Component is about to be destroyed", "color:green");
},
destroyed() {
console.log("%cDestroyed hook: Component is destroyed", "color:green");
}
});
a {
color: #659B5E;
}
a:hover, a:focus {
color: #3c5c37;
}
.btn {
font-size: 12px;
letter-spacing: 0.5px;
padding: 6px 18px;
text-transform: uppercase; }
.wrapper {
position: relative;
width: 100%;
}
.charts {
position: relative;
top: 0%;
text-align: center;
vertical-align: middle;
cursor: move;
transition: all 1s;
padding: 0px 10px 0px;
margin-bottom: 20px; }
.chart {
position: relative;
}
.chart-left {
float: left;
margin-right: 0.5%; }
.chart-right {
float: right;
margin-left: 0.5%; }
.bar-wrapper {
position: relative; }
.bar {
position: relative;
padding: 5px 0px;
margin-bottom: 30px;
height: 34px;
font-size: 1.2em;
opacity: 1;
-webkit-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.2);
-moz-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.2);
box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.2); }
.bar.parent {
cursor: pointer; }
.bar.parent .bar-label > a:after {
content: '\02026'; }
.bar:not(.parent) {
cursor: default; }
.chart-left .bar-wrapper {
transform-origin: right center;
height: 64px;
}
.chart-right .bar-wrapper {
transform-origin: left center;
height: 64px;
}
.bar-label {
position: absolute;
white-space: nowrap;
overflow: visible;
width: 100%;
pointer-events: none; }
.bar-label > a {
color: white;
display: inline-block;
max-width: 100%;
transition: transform 0.2s, color 0.2s, text-shadow 0.2s;
pointer-events: auto;
text-decoration: none; }
.bar-label > a:hover {
text-decoration: underline; }
.bar-label > a.no-description {
text-decoration: none;
cursor: default; }
.bar-label .popover-footer {
padding: 9px 14px 9px;
font-size: 12px;
background-color: #f8f8f8;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-areas: "type edit"; }
.bar-label .popover-footer .node-type {
grid-area: type;
color: #bbb;
text-transform: uppercase; }
.bar-label .popover-footer .node-type .fa, .bar-label .popover-footer .node-type .fas, .bar-label .popover-footer .node-type .far {
margin-right: 10px; }
.bar-label .popover-footer .node-edit {
grid-area: edit;
text-align: right; }
.bar-label .popover-footer .node-edit a {
pointer-events: auto;
display: none; }
.chart-left .bar-fluid .bar-label {
left: 0%;
padding-left: 22px; }
.chart-left .bar-fixed .bar-label {
left: 0%;
padding-left: 12px; }
.chart-left .compare {
right: 0%;
background-color: #e0e0e0;
padding-left: 12px;
text-align: left;
border-right: 1px solid #f8f8f8;
margin-right: 0;
margin-left: auto; }
.chart-left .bar-label .popover {
left: -40px !important;
right: auto !important; }
.chart-left .bar-label > a {
text-align: left;
text-shadow: -3px 0px 4px #f46d43, 3px 0px 4px #f46d43, 0px 3px 4px #f46d43, 0px -3px 4px #f46d43; }
.chart-left .bar-label > a.outside {
text-align: right; }
.charts .bar-label > a.outside {
text-shadow: none;
color: #999; }
.chart-left .bar {
background: linear-gradient(#f14813 4%, #f46d43 5%, #f14813 95%, #f79273 96%);
background-position: left top;
border-right: none;
text-align: left;
margin-left: auto;
margin-right: 0px;
overflow: visible !important; }
.chart-left .bar:hover .sub-bar {
background-color: rgba(0, 0, 0, 0.15);
box-shadow: inset 0 0 2px #ffffff; }
.chart-left bar.bar-fluid {
background: linear-gradient(#f14813 4%, #f46d43 5%, #f14813 95%, #f79273 96%); }
.chart-left .bar-fixed {
background: repeating-linear-gradient(45deg, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.2) 10px, transparent 10px, transparent 20px), linear-gradient(#f14813 4%, #f46d43 5%, #f14813 95%, #f79273 96%); }
.bar-fixed .bar-handle {
display: none; }
.chart-left .bar.hiding {
-webkit-animation: remove-left 0.5s ease forwards;
/* Chrome, Safari, Opera */
animation: remove-left 0.5s ease forwards; }
.chart-left .bar.showing {
-webkit-animation: add-left 0.5s ease forwards;
/* Chrome, Safari, Opera */
animation: add-left 0.5s ease forwards; }
.amount, .charts .percentage {
position: absolute;
white-space: nowrap; }
.chart-left .bar-fixed .amount, .chart-left .bar-fixed .percentage {
text-align: right;
right: 100%;
padding-right: 10px; }
.chart-left .bar-fluid .amount, .chart-left .bar-fluid .percentage, .chart-left .compare .amount, .chart-left .compare .percentage {
text-align: right;
right: 100%;
padding-right: 22px; }
.chart-right .bar-fixed .amount, .chart-right .bar-fixed .percentage {
text-align: left;
left: 100%;
padding-left: 10px; }
.chart-right .bar-fluid .amount, .chart-right .bar-fluid .percentage, .chart-right .compare .amount, .chart-right .compare .percentage {
text-align: left;
left: 100%;
padding-left: 22px; }
.shadowed {
box-shadow: 1px 2px 5px 1px rgba(0, 0, 0, 0.1); }
.chart-left .line, .chart-left .sub-bar {
right: 0px;
border-left: 1px solid rgba(255, 255, 255, 0.2);
border-right: 1px solid rgba(255, 255, 255, 0.2); }
.chart-right .line, .chart-right .sub-bar {
left: 0px;
border-left: 1px solid rgba(255, 255, 255, 0.2);
border-right: 1px solid rgba(255, 255, 255, 0.2); }
.sub-bar {
height: 34px;
position: absolute;
top: 0px;
opacity: 1;
text-align: center; }
.sub-bar span {
display: none; }
.chart-left .sub-bar {
right: 0px;
}
.chart-right .sub-bar {
left: 0px;
}
body {
margin: 50px;
}
.chart {
float: none;
cursor: pointer;
border: 1px solid lightgrey;
background-color: #eee;
}
.bars {
height: 300px;
}
.bar {
height: 34px;
margin-bottom: 26px;
}
.sub-bar {
display: inline-block;
}
.list-move {
transition: transform 1s;
}
.tools {
margin: 30px auto;
text-align: center;
}
.tools button {
display: inline-block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script type="text/x-template" id="bar-wrapper">
<div>
<div class="bar bar-fluid" v-bind:style="{display: 'flex', 'flex-direction': 'row-reverse', 'padding': 0, transition: 'none', width: amount(node) + '%'}">
<div class="sub-bar" v-for="(no, index) in node.children" v-bind:key="no.id" v-bind:style="{position: 'static', width: amount(no) + '%'}"></div>
<div class="bar-label" style="top: 5px">
<a v-if="node.children" #click.stop.prevent="drillDown(side, node, $event)">{{node.name}}...</a>
<span v-else>{{node.name}}</span>
</div>
</div>
</div>
</script>
<div id="main">
<center>
<p>
<i>Click on a bar label to drill down. Click on the chart background to come back up.</i>
</p>
<b>
<p>
<u>How to reproduce:</u> Click on <kbd>Fish...</kbd>, then click on <kbd>Delete Fish</kbd> button.
</p>
<p>
<u>Question:</u> Why does the view not update when I am looking at Cod and Salmon (inside Fish), and I delete the Fish node?
</p>
</b>
</center>
<div class="tools">
<button #click="deleteFish">Delete Fish</button>
<button #click="deleteChild">Delete first child</button>
<button #click="insertCake">Insert Cake</button>
<button #click="sortMe">Sort</button>
<button #click="init">Reset</button>
</div>
<div class="charts">
<div class="chart chart-left" #click="drillUp(0, expenses, $event)">
<transition-group name="list" tag="div" class="bars">
<bar-wrapper v-on:drill-to="drillDown" v-for="(node, index) in expenses_view_nodes" :key="node.id" class="wrapper" :node="node" :index="index" :side="0" :style="{position: 'absolute', top: index * 60 + 'px'}"></bar-wrapper>
</transition-group>
</div>
</div>
</div>

In JS, splicing an element from an array doesn't remove it from memory, so my expenses_view_nodes is still valid, eventhough the node's been removed from the parent's children array.
The way I resolve this is - after deleting the Fish node - to check whether the view pointer (expenses_view_nodes) points to a node that is still in the tree. If not, change it to point to Fish's parent node (id = 100). Vue then redraws as expected.

When removing items from an array in Vue, try wrapping the actual .splice operation in setTimeout.
Instead of something like:
del: function(item) {
this.list.splice(this.list.indexOf(item), 1);
}
do this:
del: function(item) {
var list = this.list, index = list.indexOf(item);
setTimeout(function() { list.splice(index, 1); }, 0);
}

Related

How can I bind two classes on two distinct conditions in Vue.js?

I am working on a small checkout stepper with Vue (v 2.x.x).
The current item should have the class name "active" while all the previous ones have the class name "completed", as illustrated bellow:
* {
margin: 0;
padding: 0;
font-family: "Poppins", sans-serif;
}
.progressbar {
display: flex;
list-style-type: none;
counter-reset: steps;
padding-top: 50px;
justify-content: space-between;
}
.progressbar li {
font-size: 13px;
text-align: center;
position: relative;
flex-grow: 1;
flex-basis: 0;
color: rgba(0, 0, 0, 0.5);
font-weight: 600;
}
.progressbar li.completed {
color: #ccc;
}
.progressbar li.active {
color: #4caf50;
}
.progressbar li::after {
counter-increment: steps;
content: counter(steps, decimal);
display: block;
width: 30px;
height: 30px;
line-height: 30px;
border: 2px solid rgba(0, 0, 0, 0.5);
background: #fff;
border-radius: 50%;
position: absolute;
left: 50%;
margin-left: -15px;
margin-top: -60px;
}
.progressbar li.active::after,
.progressbar li.completed::after {
background: #4caf50;
border-color: rgba(0, 0, 0, 0.15);
color: #fff;
}
.progressbar li.completed::after {
content: '\2713';
}
.progressbar li::before {
content: "";
position: absolute;
top: -26px;
left: -50%;
width: 100%;
height: 2px;
background: rgba(0, 0, 0, 0.5);
z-index: -1;
}
.progressbar li.active::before,
.progressbar li.completed::before,
.progressbar li.active+li::before {
background: #4caf50;
}
.progressbar li:first-child::before {
display: none;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet"/>
<div class="container">
<ul class="progressbar">
<li class="completed">Shopping cart</li>
<li class="completed">Shipping</li>
<li class="active">Payment</li>
<li>Confirmation</li>
</ul>
</div>
The problem
In Vue, using v-bind:class I was able to add the "active" class.
I can add the "completed" class to the proper elements:
<li v-for="(step, index) in steps" v-bind:class="{completed: step.completed === true}">{{step.text}}</li>
But because I have not found a way to bind two classes on two distinct conditions, I can not do both these things:
var app = new Vue({
el: "#cart",
data: {
stepCounter: 1,
steps: [
{ step: 1, completed: false, text: "Shopping cart" },
{ step: 2, completed: false, text: "Shipping" },
{ step: 3, completed: false, text: "Payment" },
{ step: 4, completed: false, text: "Confirmation" }
]
},
mounted() {},
methods: {
doPrev: function() {
if (this.stepCounter > 1) {
this.stepCounter--;
this.doCompleted();
}
},
doNext: function() {
if (this.stepCounter < this.steps.length) {
this.stepCounter++;
this.doCompleted();
}
},
doCompleted: function() {
this.steps.forEach(item => {
if(item.step < this.stepCounter){
item.completed = true;
}
});
}
}
});
* {
margin: 0;
padding: 0;
font-family: "Poppins", sans-serif;
}
.progressbar {
display: flex;
list-style-type: none;
counter-reset: steps;
padding-top: 50px;
justify-content: space-between;
}
.progressbar li {
font-size: 13px;
text-align: center;
position: relative;
flex-grow: 1;
flex-basis: 0;
color: rgba(0, 0, 0, 0.5);
font-weight: 600;
}
.progressbar li.completed {
color: #ccc;
}
.progressbar li.active {
color: #4caf50;
}
.progressbar li::after {
counter-increment: steps;
content: counter(steps, decimal);
display: block;
width: 30px;
height: 30px;
line-height: 30px;
border: 2px solid rgba(0, 0, 0, 0.5);
background: #fff;
border-radius: 50%;
position: absolute;
left: 50%;
margin-left: -15px;
margin-top: -60px;
}
.progressbar li.active::after,
.progressbar li.completed::after {
background: #4caf50;
border-color: rgba(0, 0, 0, 0.15);
color: #fff;
}
.progressbar li.completed::after {
content: '\2713';
}
.progressbar li::before {
content: "";
position: absolute;
top: -26px;
left: -50%;
width: 100%;
height: 2px;
background: rgba(0, 0, 0, 0.5);
z-index: -1;
}
.progressbar li.active::before,
.progressbar li.completed::before,
.progressbar li.active+li::before {
background: #4caf50;
}
.progressbar li:first-child::before {
display: none;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>
<div id="cart">
<div class="container">
<ul class="progressbar">
<li v-for="(step, index) in steps" v-bind:class="{active: index + 1 === stepCounter}">{{step.text}}</li>
</ul>
</div>
<div class="container mt-5 text-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-success" #click="doPrev()">Previous</button>
<button type="button" class="btn btn-sm btn-success" #click="doNext()">Next</button>
</div>
</div>
</div>
How can I fix this issue?
When you are binding the class it uses a javascript object where you can have multiple properties.
That means that you can assign multiple classes:
<li v-for="(step, index) in steps" v-bind:class="{ active: index + 1 === stepCounter, completed : index < stepCounter }">{{step.text}}</li>
Class binding
var app = new Vue({
el: "#cart",
data: {
stepCounter: 1,
steps: [
{ step: 1, completed: false, text: "Shopping cart" },
{ step: 2, completed: false, text: "Shipping" },
{ step: 3, completed: false, text: "Payment" },
{ step: 4, completed: false, text: "Confirmation" }
]
},
mounted() {},
methods: {
doPrev: function() {
if (this.stepCounter > 1) {
this.stepCounter--;
}
},
doNext: function() {
if (this.stepCounter < this.steps.length) {
this.stepCounter++;
this.doCompleted();
}
},
doCompleted: function() {
this.steps.forEach(item => {
if(item.step < this.stepCounter){
item.completed = true;
}
});
}
}
});
* {
margin: 0;
padding: 0;
font-family: "Poppins", sans-serif;
}
.progressbar {
display: flex;
list-style-type: none;
counter-reset: steps;
padding-top: 50px;
justify-content: space-between;
}
.progressbar li {
font-size: 13px;
text-align: center;
position: relative;
flex-grow: 1;
flex-basis: 0;
color: rgba(0, 0, 0, 0.5);
font-weight: 600;
}
.progressbar li.completed {
color: #ccc;
}
.progressbar li.active {
color: #4caf50;
}
.progressbar li::after {
counter-increment: steps;
content: counter(steps, decimal);
display: block;
width: 30px;
height: 30px;
line-height: 30px;
border: 2px solid rgba(0, 0, 0, 0.5);
background: #fff;
border-radius: 50%;
position: absolute;
left: 50%;
margin-left: -15px;
margin-top: -60px;
}
.progressbar li.active::after,
.progressbar li.completed::after {
background: #4caf50;
border-color: rgba(0, 0, 0, 0.15);
color: #fff;
}
.progressbar li.completed::after {
content: '\2713';
}
.progressbar li::before {
content: "";
position: absolute;
top: -26px;
left: -50%;
width: 100%;
height: 2px;
background: rgba(0, 0, 0, 0.5);
z-index: -1;
}
.progressbar li.active::before,
.progressbar li.completed::before,
.progressbar li.active+li::before {
background: #4caf50;
}
.progressbar li:first-child::before {
display: none;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>
<div id="cart">
<div class="container">
<ul class="progressbar">
<li v-for="(step, index) in steps" v-bind:class="{active: index + 1 === stepCounter, completed : index < stepCounter }">{{step.text}}</li>
</ul>
</div>
<div class="container mt-5 text-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-success" #click="doPrev()">Previous</button>
<button type="button" class="btn btn-sm btn-success" #click="doNext()">Next</button>
</div>
</div>
</div>
you can add more classes to the condition you need by using an array on the bind, like this:
<li v-for="(step, index) in steps" v-bind:class="[{completed: index < stepCounter-1},{active: index + 1 === stepCounter}]">{{step.text}}</li>
please see amended below code, you can also use a method to generate the class based on the arguments this keeps the markup a little cleaner and a central method which generates the correct classes.
var app = new Vue({
el: "#cart",
data: {
stepCounter: 1,
steps: [
{ step: 1, completed: false, text: "Shopping cart" },
{ step: 2, completed: false, text: "Shipping" },
{ step: 3, completed: false, text: "Payment" },
{ step: 4, completed: false, text: "Confirmation" }
]
},
mounted() {},
methods: {
doPrev: function() {
if (this.stepCounter > 1) {
this.stepCounter--;
}
},
doNext: function() {
if (this.stepCounter < this.steps.length) {
this.stepCounter++;
this.doCompleted();
}
},
doCompleted: function() {
this.steps.forEach(item => {
if(item.step < this.stepCounter){
item.completed = true;
}
});
},
getClass: function(index, step) {
var values = [];
if (index + 1 === this.stepCounter) values.push('active');
if (step.completed) values.push('completed');
return values.join(' ');
}
}
});
* {
margin: 0;
padding: 0;
font-family: "Poppins", sans-serif;
}
.progressbar {
display: flex;
list-style-type: none;
counter-reset: steps;
padding-top: 50px;
justify-content: space-between;
}
.progressbar li {
font-size: 13px;
text-align: center;
position: relative;
flex-grow: 1;
flex-basis: 0;
color: rgba(0, 0, 0, 0.5);
font-weight: 600;
}
.progressbar li.completed {
color: #ccc;
}
.progressbar li.active {
color: #4caf50;
}
.progressbar li::after {
counter-increment: steps;
content: counter(steps, decimal);
display: block;
width: 30px;
height: 30px;
line-height: 30px;
border: 2px solid rgba(0, 0, 0, 0.5);
background: #fff;
border-radius: 50%;
position: absolute;
left: 50%;
margin-left: -15px;
margin-top: -60px;
}
.progressbar li.active::after,
.progressbar li.completed::after {
background: #4caf50;
border-color: rgba(0, 0, 0, 0.15);
color: #fff;
}
.progressbar li.completed::after {
content: '\2713';
}
.progressbar li::before {
content: "";
position: absolute;
top: -26px;
left: -50%;
width: 100%;
height: 2px;
background: rgba(0, 0, 0, 0.5);
z-index: -1;
}
.progressbar li.active::before,
.progressbar li.completed::before,
.progressbar li.active+li::before {
background: #4caf50;
}
.progressbar li:first-child::before {
display: none;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>
<div id="cart">
<div class="container">
<ul class="progressbar">
<li v-for="(step, index) in steps" v-bind:class="getClass(index, step)">{{step.text}}</li>
</ul>
</div>
<div class="container mt-5 text-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-success" #click="doPrev()">Previous</button>
<button type="button" class="btn btn-sm btn-success" #click="doNext()">Next</button>
</div>
</div>
</div>

Multi-file upload to Amazon S3

I have a webpage and so far it works for single file uploads, but I need to be able to upload multiple files at the same time. Amazon doesn't like this and gives me "POST requires exactly one file upload per request.", I would assume I'd just need to make a seperate POST request for every file, but I don't know how to. I'm just using simple HTML and ajax, and would like to stick to that.
#import " '_normalize.css' ";
#import " '_defaults.css' ";
body,
.ad,
.sm {
font-family: Lucida Grande, Helvetica Neue, Helvetica, Arial, Verdana, sans-serif
}
a {
color: currentColor;
text-decoration: none
}
.clearfix::after {
content: '';
display: table;
clear: both
}
.ad {
width: 9.375rem;
color: #444;
color: rgba( 0, 0, 0, .75);
background-color: #fff;
background-color: rgba( 255, 255, 255, .5);
position: fixed;
z-index: 1000;
top: .625rem;
left: .625rem;
padding: .5rem .625rem
}
.ad--dark {
color: #ddd;
color: rgba( 255, 255, 255, .75);
background-color: #111;
background-color: rgba( 0, 0, 0, .5)
}
.ad__close {
width: .625rem;
height: .625rem;
background-color: #777;
background-color: rgba( 0, 0, 0, .5);
border-radius: 50%;
position: absolute;
z-index: 1;
top: -.25rem;
right: -.25rem;
-webkit-transition: -webkit-transform .25s ease-in-out;
transition: transform .25s ease-in-out
}
.ad--dark .ad__close {
background-color: #ddd;
background-color: rgba( 255, 255, 255, .75)
}
.ad__close:hover,
.ad__close:focus {
-webkit-transform: scale( 1.25);
-ms-transform: scale( 1.25);
transform: scale( 1.25)
}
#carbonads {
font-size: .875rem;
letter-spacing: -.071em;
line-height: 1.125rem
}
#carbonads a {
color: currentColor;
display: block;
margin-top: .313rem
}
#carbonads .carbon-poweredby {
font-size: .75rem;
text-transform: uppercase;
color: #aaa;
color: rgba( 0, 0, 0, .25)
}
.ad--dark #carbonads .carbon-poweredby {
color: #999;
color: rgba( 255, 255, 255, .25)
}
.sm {
width: 100%;
height: 2.5rem;
color: #444;
color: rgba( 0, 0, 0, .75);
background-color: #fff;
background-color: rgba( 255, 255, 255, .5);
overflow: hidden;
position: fixed;
z-index: 1001;
bottom: 0;
left: 0;
padding: .625rem 1.25rem 0
}
.sm--dark {
color: #ddd;
color: rgba( 255, 255, 255, .75);
background-color: #111;
background-color: rgba( 0, 0, 0, .5)
}
.sm ul {}
.sm li {
float: right;
margin-left: 1rem
}
.sm li:first-child {
float: left;
margin-left: 0
}
.sm .googleplus-one {
max-width: 60px
}
.sm .twitter-follow>*:not( :first-child),
.sm .twitter-share>*:not( :first-child) {
display: none
}
#media screen {
#media(min-width: 0px) {
.sm li:last-child {
opacity:0;
-webkit-transition: opacity .25s ease-in-out;
transition: opacity .25s ease-in-out
}
.sm:hover li:last-child {
opacity: 1
}
}
}
.sm__back {
font-size: .875rem;
font-weight: 700;
color: currentColor;
text-transform: uppercase;
position: relative
}
.sm__back::before {
width: 0;
height: 0;
border: .313rem solid transparent;
border-left: none;
border-right-color: currentColor;
content: '';
display: inline-block;
position: relative;
left: 0;
margin-right: .313rem;
-webkit-transition: left .25s ease-in-out;
transition: left .25s ease-in-out
}
.sm__back:hover::before,
.sm__back:focus::before {
left: -.313rem
}
#media screen and (max-width:40em),
screen and (max-height:40em) {
.ad,
.sm {
display: none
}
}
<!DOCTYPE html>
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<title>TEST</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="stylesheet" href="main.css" />
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,300italic,400" />
<style>
html {}
body {
font-family: Roboto, sans-serif;
color: #0f3c4b;
background-color: #e5edf1;
padding: 5rem 1.25rem;
/* 80 20 */
}
.container {
width: 100%;
max-width: 680px;
/* 800 */
text-align: center;
margin: 0 auto;
}
.container h1 {
font-size: 42px;
font-weight: 300;
color: #0f3c4b;
margin-bottom: 40px;
}
.container h1 a:hover,
.container h1 a:focus {
color: #39bfd3;
}
.container nav {
margin-bottom: 40px;
}
.container nav a {
border-bottom: 2px solid #c8dadf;
display: inline-block;
padding: 4px 8px;
margin: 0 5px;
}
.container nav a.is-selected {
font-weight: 700;
color: #39bfd3;
border-bottom-color: currentColor;
}
.container nav a:not( .is-selected):hover,
.container nav a:not( .is-selected):focus {
border-bottom-color: #0f3c4b;
}
.container footer {
color: #92b0b3;
margin-top: 40px;
}
.container footer p+p {
margin-top: 1em;
}
.container footer a:hover,
.container footer a:focus {
color: #39bfd3;
}
.box {
font-size: 1.25rem;
/* 20 */
background-color: #c8dadf;
position: relative;
padding: 100px 20px;
}
.box.has-advanced-upload {
outline: 2px dashed #92b0b3;
outline-offset: -10px;
-webkit-transition: outline-offset .15s ease-in-out, background-color .15s linear;
transition: outline-offset .15s ease-in-out, background-color .15s linear;
}
.box.is-dragover {
outline-offset: -20px;
outline-color: #c8dadf;
background-color: #fff;
}
.box__dragndrop,
.box__icon {
display: none;
}
.box.has-advanced-upload .box__dragndrop {
display: inline;
}
.box.has-advanced-upload .box__icon {
width: 100%;
height: 80px;
fill: #92b0b3;
display: block;
margin-bottom: 40px;
}
.box.is-uploading .box__input,
.box.is-success .box__input,
.box.is-error .box__input {
visibility: hidden;
}
.box__uploading,
.box__success,
.box__error {
display: none;
}
.box.is-uploading .box__uploading,
.box.is-success .box__success,
.box.is-error .box__error {
display: block;
position: absolute;
top: 50%;
right: 0;
left: 0;
-webkit-transform: translateY( -50%);
transform: translateY( -50%);
}
.box__uploading {
font-style: italic;
}
.box__success {
-webkit-animation: appear-from-inside .25s ease-in-out;
animation: appear-from-inside .25s ease-in-out;
}
#-webkit-keyframes appear-from-inside {
from {
-webkit-transform: translateY( -50%) scale( 0);
}
75% {
-webkit-transform: translateY( -50%) scale( 1.1);
}
to {
-webkit-transform: translateY( -50%) scale( 1);
}
}
#keyframes appear-from-inside {
from {
transform: translateY( -50%) scale( 0);
}
75% {
transform: translateY( -50%) scale( 1.1);
}
to {
transform: translateY( -50%) scale( 1);
}
}
.box__restart {
font-weight: 700;
}
.box__restart:focus,
.box__restart:hover {
color: #39bfd3;
}
.js .box__file {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
.js .box__file+label {
max-width: 80%;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
display: inline-block;
overflow: hidden;
}
.js .box__file+label:hover strong,
.box__file:focus+label strong,
.box__file.has-focus+label strong {
color: #39bfd3;
}
.js .box__file:focus+label,
.js .box__file.has-focus+label {
outline: 1px dotted #000;
outline: -webkit-focus-ring-color auto 5px;
}
.js .box__file+label * {
/* pointer-events: none; */
/* in case of FastClick lib use */
}
.no-js .box__file+label {
display: none;
}
.no-js .box__button {
display: block;
}
.box__button {
font-weight: 700;
color: #e5edf1;
background-color: #39bfd3;
display: block;
padding: 8px 16px;
margin: 40px auto 0;
}
.box__button:hover,
.box__button:focus {
background-color: #0f3c4b;
}
</style>
</head>
<body>
<div class="container" role="main">
<form action="https://s3-[removed].amazonaws.com/[removed]" method="post" enctype="multipart/form-data" novalidate class="box">
<input type="hidden" name="key" value="uploads/${filename}">
<input Access Key ID type="hidden" type="input" name="AWSAccessKeyId" value="[removed]">
<input Signature type="hidden" type="password" name="signature" value="[removed]">
<input type="hidden" name="acl" value="private">
<input type="hidden" name="success_action_redirect" value="[removed]">
<input type="hidden" name="x-amz-server-side-encryption" value="AES256" />
<input type="hidden" name="policy" value="[removed]" <!-- Include any additional input fields here -->
<input type="input" class="text-input" name="x-amz-meta-tag" value="" placeholder="Your Name Here" />
<div class="box__input">
<input type="file" name="file" id="file" class="box__file" data-multiple-caption="{count} files selected" multiple />
<label for="file"><strong>Choose a file</strong><span class="box__dragndrop"> or drag it here</span>.</label>
<button type="submit" class="box__button">Upload</button>
</div>
<div class="box__uploading">Uploading…</div>
</form>
</div>
<script type="8f8d05b8d77097cd667d97f3-text/javascript">
'use strict';
;
(function(document, window, index) {
// feature detection for drag&drop upload
var isAdvancedUpload = function() {
var div = document.createElement('div');
return (('draggable' in div) || ('ondragstart' in div && 'ondrop' in div)) && 'FormData' in window && 'FileReader' in window;
}();
// applying the effect for every form
var forms = document.querySelectorAll('.box');
Array.prototype.forEach.call(forms, function(form) {
var input = form.querySelector('input[type="file"]'),
label = form.querySelector('label'),
errorMsg = form.querySelector('.box__error span'),
restart = form.querySelectorAll('.box__restart'),
droppedFiles = false,
showFiles = function(files) {
label.textContent = files.length > 1 ? (input.getAttribute('data-multiple-caption') || '').replace('{count}', files.length) : files[0].name;
},
triggerFormSubmit = function() {
var event = document.createEvent('HTMLEvents');
event.initEvent('submit', true, false);
form.dispatchEvent(event);
};
// letting the server side to know we are going to make an Ajax request
var ajaxFlag = document.createElement('input');
ajaxFlag.setAttribute('type', 'hidden');
ajaxFlag.setAttribute('name', 'ajax');
ajaxFlag.setAttribute('value', 1);
form.appendChild(ajaxFlag);
// automatically submit the form on file select
input.addEventListener('change', function(e) {
showFiles(e.target.files);
});
// drag&drop files if the feature is available
if (isAdvancedUpload) {
form.classList.add('has-advanced-upload'); // letting the CSS part to know drag&drop is supported by the browser
['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop'].forEach(function(event) {
form.addEventListener(event, function(e) {
// preventing the unwanted behaviours
e.preventDefault();
e.stopPropagation();
});
});
['dragover', 'dragenter'].forEach(function(event) {
form.addEventListener(event, function() {
form.classList.add('is-dragover');
});
});
['dragleave', 'dragend', 'drop'].forEach(function(event) {
form.addEventListener(event, function() {
form.classList.remove('is-dragover');
});
});
form.addEventListener('drop', function(e) {
droppedFiles = e.dataTransfer.files; // the files that were dropped
showFiles(droppedFiles);
});
}
// if the form was submitted
form.addEventListener('submit', function(e) {
// preventing the duplicate submissions if the current one is in progress
if (form.classList.contains('is-uploading')) return false;
form.classList.add('is-uploading');
form.classList.remove('is-error');
if (isAdvancedUpload) // ajax file upload for modern browsers
{
e.preventDefault();
// gathering the form data
var ajaxData = new FormData(form);
if (droppedFiles) {
Array.prototype.forEach.call(droppedFiles, function(file) {
ajaxData.append(input.getAttribute('name'), file);
});
}
// ajax request
var ajax = new XMLHttpRequest();
ajax.open(form.getAttribute('method'), form.getAttribute('action'), true);
ajax.onload = function() {
form.classList.remove('is-uploading');
if (ajax.status >= 200 && ajax.status < 400) {
var data = JSON.parse(ajax.responseText);
form.classList.add(data.success == true ? 'is-success' : 'is-error');
if (!data.success) errorMsg.textContent = data.error;
} else alert(ajax.response);
};
ajax.onerror = function() {
form.classList.remove('is-uploading');
};
ajax.send(ajaxData);
} else // fallback Ajax solution upload for older browsers
{
var iframeName = 'uploadiframe' + new Date().getTime(),
iframe = document.createElement('iframe');
$iframe = $('<iframe name="' + iframeName + '" style="display: none;"></iframe>');
iframe.setAttribute('name', iframeName);
iframe.style.display = 'none';
document.body.appendChild(iframe);
form.setAttribute('target', iframeName);
iframe.addEventListener('load', function() {
var data = JSON.parse(iframe.contentDocument.body.innerHTML);
form.classList.remove('is-uploading')
form.classList.add(data.success == true ? 'is-success' : 'is-error')
form.removeAttribute('target');
if (!data.success) errorMsg.textContent = data.error;
iframe.parentNode.removeChild(iframe);
});
}
});
// restart the form if has a state of error/success
Array.prototype.forEach.call(restart, function(entry) {
entry.addEventListener('click', function(e) {
e.preventDefault();
form.classList.remove('is-error', 'is-success');
input.click();
});
});
// Firefox focus bug fix for file input
input.addEventListener('focus', function() {
input.classList.add('has-focus');
});
input.addEventListener('blur', function() {
input.classList.remove('has-focus');
});
});
}(document, window, 0));
</script>
<script src="https://ajax.cloudflare.com/cdn-cgi/scripts/7089c43e/cloudflare-static/rocket-loader.min.js" data-cf-settings="8f8d05b8d77097cd667d97f3-|49" defer=""></script>
</body>
<div class="background"></div>
</html>

Javascript focus() is not working in mobile browser on element that just entered the window from top, React functional component

I've built a search bar dropdown as part of a navbar I'm working on in React. The search bar's starting position is above the window, top: -112px. When the search button on the nav is clicked, the search bar animation begins and the the drop down goes from top: -112px to top: 0. After the animation is complete and the 'shown' class is applied to the search bar container, a useEffect watching the displayClass value triggers and focuses on the search bar input field.
This works fine on desktop Chrome and Safari, but on mobile the input field does not focus once in view for either browser. I've tried setTimeouts of every length on the focus call to no avail. Any help would be greatly appreciated.
SearchBar component:
export const SearchBar = ({
analyticsSearchBarSearchInput,
display,
history,
onClose,
}) => {
const [displayClass, setDisplayClass] = useState("");
const [searchActive, setSearchActive] = useState(false);
const prevDisplayRef = useRef();
useEffect(() => {
prevDisplayRef.current = display;
});
const prevDisplay = prevDisplayRef.current;
useEffect(() => {
if (!prevDisplay && display) {
onSearchOpen();
} else if (prevDisplay && !display) {
onSearchClose();
}
}, [display, prevDisplay]);
useEffect(() => {
if (displayClass === "shown") {
document.getElementById("search-bar-input").focus({
preventScroll: true,
});
}
}, [displayClass]);
return (
<ConditionalWrapper
condition={displayClass === "shown"}
wrapper={(children) => (
<OutsideClick onClick={onClose}>{children}</OutsideClick>
)}
>
<div className={`search-bar__container ${displayClass}`}>
<div className="search-bar__left">
<div className={`search-bar__search-icon${getSearchIconClass()}`} />
<div className="search-bar__input">
<SearchForm
onSubmit={onSearchSubmit}
searchActive={searchActive}
setSearchActive={setSearchActive}
/>
</div>
</div>
<div className="search-bar__right">
<div
aria-label="Close search"
className="search-bar__close-icon"
onClick={onClose}
onKeyDown={onClose}
role="button"
tabIndex={0}
/>
</div>
</div>
</ConditionalWrapper>
);
function getSearchIconClass() {
return searchActive ? " active" : "";
}
function onSearchClose() {
setDisplayClass("hide");
setTimeout(() => {
setDisplayClass("");
}, 300);
enableBodyScroll();
document.body.removeEventListener("touchmove", touchMoveCallback, {
passive: false,
});
}
function onSearchOpen() {
setDisplayClass("show");
setTimeout(() => {
setDisplayClass("shown");
}, 200);
disableBodyScroll();
document.body.addEventListener("touchmove", touchMoveCallback, {
passive: false,
});
}
function onSearchSubmit(usersInput) {
const hostUrl = window.location.host;
const pageNumber = 1;
const searchUrl = `${hostUrl}/search?query=${usersInput}&page=${pageNumber}`;
if (displayClass === "shown" && usersInput !== "") {
Promise.resolve(
analyticsSearchBarSearchInput(hostUrl, usersInput, searchUrl)
).then(() => {
onClose();
history.push(`/search?query=${usersInput}&page=${pageNumber}`);
});
}
}
};
const ConditionalWrapper = ({ condition, wrapper, children }) =>
condition ? wrapper(children) : children;
const touchMoveCallback = (e) => {
e.preventDefault();
};
SearchBar Form Component:
export const SearchForm = ({ onSubmit, searchActive, setSearchActive }) => {
const [search, setSearch] = useState("");
useEffect(() => {
search !== "" ? setSearchActive(true) : setSearchActive(false);
}, [search, setSearchActive]);
return (
<div className="search-bar__form-container">
<form action="#" onSubmit={onSearchSubmit} className="search-bar__form">
<input
autoComplete="off"
name="search"
className="search-bar__input"
id="search-bar-input"
onChange={handleChange}
placeholder="Search"
type="search"
value={search}
onKeyDown={onKeyDown}
/>
</form>
{searchActive && (
<div
aria-label="Search submit"
className="search-bar__submit-button"
onClick={onSearchSubmit}
onKeyDown={onKeyDown}
role="button"
tabIndex={0}
>
SEARCH>
</div>
)}
</div>
);
function handleChange(event) {
setSearch(event.target.value);
}
function onSearchSubmit() {
onSubmit(search);
setSearch("");
document.activeElement.blur();
}
function onKeyDown(event) {
if (event.keyCode === 13) {
event.preventDefault();
onSearchSubmit();
}
}
};
Styling:
.search-bar {
&__close-icon {
background: url("/images/icons/close.png") no-repeat;
background-size: contain;
cursor: pointer;
height: 26px;
transition: all 80ms ease-out;
width: 26px;
&:hover {
background: url("/images/icons/close-hover.png") no-repeat;
background-size: contain;
}
}
&__container {
align-items: center;
background-color: #ffffff;
border-bottom: 1px solid #d8d8d8;
color: #c5c5c5;
display: flex;
height: 112px;
justify-content: space-between;
left: 0;
position: fixed;
top: -112px;
width: 100%;
z-index: 1020;
&.show {
animation: searchFadeIn 200ms ease-out;
}
&.shown {
top: 0;
box-shadow: 0 0 0 100vmax rgba(51, 51, 51, 0.4);
}
&.hide {
animation: searchFadeOut 300ms ease-in;
}
}
#keyframes searchFadeIn {
0% {
box-shadow: 0 0 0 100vmax rgba(51, 51, 51, 0);
}
50% {
box-shadow: 0 0 0 100vmax rgba(51, 51, 51, 0.2);
}
100% {
box-shadow: 0 0 0 100vmax rgba(51, 51, 51, 0.4);
top: 0px;
}
}
#keyframes searchFadeOut {
0% {
box-shadow: 0 0 0 100vmax rgba(51, 51, 51, 0.4);
top: 0;
}
50% {
box-shadow: 0 0 0 100vmax rgba(51, 51, 51, 0.2);
}
100% {
box-shadow: 0 0 0 100vmax rgba(51, 51, 51, 0);
top: -75px;
}
}
&__icon {
background: url("/images/icons/search.png") no-repeat;
background-size: contain;
cursor: pointer;
height: 24px;
padding-right: 20px;
width: 24px;
}
&__input {
background: none;
border: none;
color: #333333;
flex: 1;
font-family: Setimo;
font-size: 42px;
line-height: 1.24;
letter-spacing: 2px;
max-width: 321px;
padding-right: 10px;
}
&__form {
margin: 0 20px 0 20px;
}
&__form-container {
display: flex;
}
&__left {
align-items: center;
display: flex;
padding-left: 40px;
}
&__right {
color: inherit;
font-size: 30px;
padding-right: 40px;
}
&__search-icon {
background: url("/images/icons/search-grey.png") no-repeat;
background-size: contain;
cursor: pointer;
height: 24px;
padding-right: 20px;
width: 24px;
&.active {
background: url("/images/icons/search.png") no-repeat;
background-size: contain;
}
}
&__submit-button {
align-self: flex-end;
color: #9a9a9a;
cursor: pointer;
flex: 1;
font-family: Setimo;
font-size: 11px;
font-weight: bold;
line-height: 1.64;
letter-spacing: 3.5px;
margin-bottom: 7px;
transition: all 80ms ease-out;
width: 81px;
&:hover {
color: #333333;
}
}
}
.search-icon {
&__container {
align-items: center;
cursor: pointer;
display: flex;
justify-content: space-between;
transition: all 80ms ease-out;
width: 97px;
&:hover .search-icon__text {
color: #7d7be4;
}
&:hover .search-icon__icon {
background: url("/images/icons/search-hover.png") no-repeat;
background-size: contain;
}
}
&__icon {
background: url("/images/icons/search.png") no-repeat;
background-size: contain;
cursor: pointer;
height: 20px;
width: 20px;
}
&__text {
color: #333333;
font-family: Setimo;
font-size: 11px;
font-weight: bold;
letter-spacing: 3.5px;
line-height: 1.64;
}
}
::placeholder {
color: #c5c5c5;
}
[type="search"] {
-webkit-appearance: textfield;
}
input[type="text"]::-ms-clear {
display: none;
width: 0;
height: 0;
}
input[type="text"]::-ms-reveal {
display: none;
width: 0;
height: 0;
}
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-results-button,
input[type="search"]::-webkit-search-results-decoration {
display: none;
}
#media (max-width: 959px) {
.search-bar {
&__container {
height: 80px;
top: -80px;
}
&__form {
margin: 0;
}
&__input {
font-size: 32px;
max-width: 225px;
}
&__left {
padding-left: 30px;
}
&__right {
padding-right: 30px;
}
&__search-icon {
padding-right: 14px;
}
}
}
#media (max-width: 480px) {
.search-bar {
&__close-icon {
height: 20px;
width: 20px;
}
&__container {
height: 64px;
top: -64px;
}
&__form {
margin: 0;
}
&__input {
font-size: 24px;
letter-spacing: normal;
max-width: 175px;
}
&__left {
padding-left: 16px;
}
&__right {
padding-right: 16px;
}
&__search-icon {
padding-right: 10px;
}
&__submit-button {
display: none;
}
}
}

Angular-Breadcrumb: parent state's label is empty

I'm using the angular-breadcrumb module to build a navigation panel.
I have a problem displaying the parent state's dynamic label: when activated the child's label is displayed and the parent's one is empty. Both states use different controllers.
What's wrong with my setup?
var parentState = {
name: 'parentState',
parent: 'parentParentState',
url: '/:department_id/object/:object_id?department_name&object_name',
ncyBreadcrumb: {
label: '<% resNavCtrl.stateLabel %>'
},
views: {
'main': {
controller: 'resourcesNavController',
controllerAs: 'resNavCtrl',
templateUrl: templateUrl
}
},
params: {
department_name: null,
object_name: null
},
};
var childState = {
name: 'childState',
parent: 'parentState',
url: '/:resourceType?resourceTypeName',
ncyBreadcrumb: {
label: '<% resCtrl.stateLabel %>'
},
views: {
'main#': {
controller: 'resourcesController',
controllerAs: 'resCtrl',
templateUrl: templateUrl
}
},
params: {
resourceType: '',
resourceTypeName: ''
}
};
You can use this breadcrumb instead
Use this directive as <div md-breadcrumb></div> or <md-breadcrumb></md-breadcrumb>
Note: This works only with Angular UI Router
md-breadcrumb.tmpl.html
<article>
<nav class="breadcrumb-block" role="navigation">
<span class="icon-breadcrumb-mobile"></span>
<div class="nav-wrapper breadcrumb-content">
<a ng-repeat="breadcrumb in breadcrumbs track by $index" ng-if="!$first" href="" class="breadcrumb" title="{{breadcrumb.url}}" ng-click="onClickingLink($event, breadcrumb)">{{breadcrumb.url | firstLetterCaps}}</a>
</div>
<div ui-view class="breadcrumb-child"></div>
</nav>
</article>
md-breadcrumb.ctrl.js
function breadcrumbController($scope, $state) {
$scope.breadcrumbs = [];
stateChanged(); // call to get initial breadcrumb
// change breadcrumb on each state change success
$scope.$on('$stateChangeSuccess', stateChanged);
// executes on $stateChangeSuccess
function stateChanged(){
$scope.breadcrumbs = getParentList($state.$current); // holds all active states
$scope.onClickingLink = onClickingLink; // holds link clicking function
$scope.breadcrumbs.reverse(); // reverse breadcrumbs child to root states
}
// executes on link click
function onClickingLink(event, breadcrumb) {
event.preventDefault(); // prevent default action
$state.go(breadcrumb.stateName); // move to state
}
// below function used to get parent states
function getParentList(state) {
var parentList = []; // holds parent states list
while(state) { // loop until root state occurs
// push into parentList array
parentList.push({'state': state, 'url': state.self.url.slice(1, state.self.url.length), 'stateName': state.toString()});
state = state.parent; // make parent as current state for loop
}
return parentList; // return parentList
}
}
md-breadcrumb.directive.js
function breadcrumbDirective() {
return {
restrict: 'EA',
templateUrl: 'md-breadcrumb.tmpl.html',
controller: breadcrumbController
}
}
md-breadcrumb.css
.breadcrumb:before {
display: none;
}
.breadcrumb-block {
box-shadow: none;
background: inherit;
height: auto;
line-height: 1.5;
margin-top: 0px;
padding-right: 12px;
}
.breadcrumb-block .breadcrumb {
color: #ffffff;
font-size: 14px;
padding: 8px 18px 8px 28px;
background: #015798;
position: relative;
display: block;
float: left;
}
.breadcrumb-block .breadcrumb:hover, .breadcrumb-block .breadcrumb:focus {
background-color: #0288D1;
outline: none;
}
.breadcrumb-block .breadcrumb:hover:after, .breadcrumb-block .breadcrumb:focus:after {
border-left: 10px solid #0288D1;
}
.breadcrumb-block .breadcrumb:first-child {
padding-left: 18px;
}
.breadcrumb-block .breadcrumb:last-child{
color: #ffffff;
/*font-size: 13px;
font-weight: 500;*/
background-color: #0288D1;
}
.breadcrumb-block .breadcrumb:last-child:after {
border-left: 10px solid #0288D1;
}
.breadcrumb-content:after {
display: block;
content: " ";
clear: both;
}
.breadcrumb-block .breadcrumb-icon:after,
.breadcrumb-block .breadcrumb:after {
content: " ";
display: block;
width: 0;
height: 0;
border-top: 20px solid transparent;
border-bottom: 20px solid transparent;
border-left: 10px solid #015798;
position: absolute;
top: 50%;
margin-top: -20px;
left: 100%;
z-index: 1;
}
.breadcrumb-block .breadcrumb-icon:before,
.breadcrumb-block .breadcrumb:before {
content: " ";
display: block;
width: 0;
height: 0;
border-top: 20px solid transparent;
border-bottom: 20px solid transparent;
border-left: 10px solid #DDDAD5;
position: absolute;
top: 50%;
margin-top: -20px;
margin-left: 1px;
left: 100%;
z-index: 1;
}
.breadcrumb-block .breadcrumb-icon:after {
border-left: 10px solid #FFF;
}
/*.breadcrumb-block .breadcrumb:last-child:after {
content: " ";
display: block;
border-left-color: #fff;
}*/
.breadcrumb-block .breadcrumb:last-child:before {
content: " ";
display: block;
}
.breadcrumb-block .breadcrumb-icon {
font-size: 16px;
color: #B8B8B8;
display: none;
padding: 10px 12px 12px 12px;
background: #fff;
width: 40px;
height: 40px;
position: relative;
box-shadow: 0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);
}
.breadcrumb-content {
box-shadow: 0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);
display: inline-block;
}
.breadcrumb-block a:focus {
border-bottom: 0px none;
}
#media only screen and (max-width: 799px) {
.breadcrumb-block .breadcrumb-icon {
display: block;
margin-bottom: 5px;
}
.breadcrumb-block .breadcrumb-content .breadcrumb {
display: block;
width: 100%;
margin-bottom: 5px;
}
.breadcrumb-block .breadcrumb:before,
.breadcrumb-block .breadcrumb:after {
/*content: "";*/
/*display: none;*/
}
.breadcrumb-block .breadcrumb {
padding-left: 18px;
box-shadow: 0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);
}
.breadcrumb-content {
display: block;
box-shadow: 0 0px 0px 0 rgba(0,0,0,0.0);
width: 100%;
}
}
.subcrumb {
color: #015798;
cursor: pointer;
}
.breadcrumb-child {
padding: 20px;
font-size: 15px;
}

Got double "onmouseover" Javascript

first can you look on those two image so you understand.
When not hover: http://s15.postimg.org/sn6rk45rf/not_Hover.png
When hover: http://s16.postimg.org/yk6beg1ad/on_Hover.png
Right now when I have my mouse over a image, both image get buttons.
But I just want each image have theve own buttons on mouse over and the other image hide the buttons.
I don't really know how to fix it, and I'm very beginner with Javascript.
Here is my HTML/CSS/Javascript codes.
var buttonNew = document.getElementsByClassName('buttonNewest');
var buttonRan = document.getElementsByClassName('buttonRandom');
function imageOver() {
for(var i = 0; i < buttonNew.length; i++) {
buttonNew[i].style.display = "block";
buttonNew[i].style.animation = "moveButtonsRight 2s";
}
for(var i = 0; i < buttonRan.length; i++) {
buttonRan[i].style.display = "block";
buttonRan[i].style.animation = "moveButtonsLeft 2s";
}
}
function imageLeave() {
for(var i = 0; i < buttonNew.length; i++) {
buttonNew[i].style.display = "none";
}
for(var i = 0; i < buttonRan.length; i++) {
buttonRan[i].style.display = "none";
}
}
.charSelect[role="Background"] {
width: 1600px;
min-height: 600px;
margin: 25px auto;
}
.charSelect[role="Background"] > h1 {
width: 300px;
margin: 0 auto;
border: dashed 2px rgba(255, 207, 0, 0.75);
text-align: center;
text-transform: uppercase;
font-size: 2.6em;
text-shadow: 2px 2px 3px rgb(0, 0, 0);
}
.charSelect[role="Characters"] {
position: relative;
display: inline-block;
width: 250px;
height: auto;
background: rgba(42, 42, 42, 0.7);
border: dashed 2px rgba(255, 207, 0, 0.4);
color: rgba(255, 207, 0, 1);
opacity: 0.6;
-webkit-transition: opacity 1s;
margin-left: 250px;
}
.charSelect[role="Characters"]:hover {
opacity: 1;
transform: scale(1.05);
}
.charSelect[role="Names"] {
width: 100%;
font-size: 1.8em;
}
.charSelect[role="Names"] > p {
margin: 0 !important;
text-align: center;
text-transform: uppercase;
text-shadow: 1px 1px 2px rgb(0, 0, 0);
}
/* Buttons */
.charSelect[role="LatestVid"], .charSelect[role="RandomVid"] {
width: 170px;
height: 45px;
background: -webkit-linear-gradient(top, rgb(255, 207, 0), rgba(255, 207, 0, 0));
text-align: center;
line-height: 45px;
color: black;
-webkit-transition: background 1s;
transition: background 1s;
box-shadow: 0px 0px 3px;
}
.charSelect[role="LatestVid"] {
display: none;
position: absolute;
top:50%;
right: 70%;
}
.charSelect[role="RandomVid"] {
display: none;
position: absolute;
top:50%;
left: 70%;
}
.charSelect[role="RandomVid"]:hover , .charSelect[role="LatestVid"]:hover {
background: rgb(255, 207, 0);
}
/* Animation */
#-webkit-keyframes moveButtonsLeft {
0% {
left: 50%;
}
100% {
left: 70%;
}
}
#-webkit-keyframes moveButtonsRight {
0% {
right: 50%;
}
100% {
right: 70%;
}
}
<!-- Character one -->
<div onmouseover="imageOver()" onmouseleave="imageLeave()" class="charSelect" role="Characters">
<img src="chars/Dekker.gif" width="250"/>
<div class="charSelect buttonNewest" role="LatestVid">Newest Videos</div>
<div class="charSelect buttonRandom" role="RandomVid">Random Videos</div>
<div class="charSelect" role="Names"><p>Dekker</p></div>
</div>
<!-- Character two -->
<div onmouseover="imageOver()" onmouseleave="imageLeave()" class="charSelect" role="Characters">
<img src="chars/Dekker.gif" width="250"/>
<div class="charSelect buttonNewest" role="LatestVid">Newest Videos</div>
<div class="charSelect buttonRandom" role="RandomVid">Random Videos</div>
<div class="charSelect" role="Names"><p>Dekker</p></div>
</div>
You're calling an imageOver() that loops all your elements.
Instead of using JS (at all) I'd go with pure CSS:
*{font: 14px/1 sans-serif;}
.charSelect{
position: relative;
display: inline-block;
vertical-align: top;
}
.charButtons{
position: absolute;
bottom: 40px;
width: 100%;
text-align:center;
opacity: 0;
visibility: hidden;
transition: 0.4s;
-webkit-transition: 0.4s;
}
.charButtons a{
display: block;
margin-top: 1px;
text-align: center;
color: #fff;
background: #444;
padding: 10px;
opacity: 0.9;
transition: 0.3s;
-webkit-transition: 0.3s;
}
.charButtons a:hover{ opacity:1; }
.charSelect:hover .charButtons{
visibility: visible;
opacity: 1;
}
<div class="charSelect">
<img src="http://placehold.it/180x150/4af/&text=Hero+1">
<div class="charButtons">
Newest Videos
Random Videos
</div>
<h2>HERO 1</h2>
</div>
<div class="charSelect">
<img src="http://placehold.it/180x150/fa4/&text=Hero+2">
<div class="charButtons">
Newest Videos
Random Videos
</div>
<h2>HERO 2</h2>
</div>
The problem is that you're not reffering tot the current object that you have cursor on. If you go with with cursor over and image, your function will apply those changes for all buttonNew and buttonRan that can be found on page.

Categories

Resources