I want to have a two fields name and shade. these two fields should be shown as
grid when i click a dropdown. and when i select a row from the grid only name should be binded to dropdown.some thing like this in the below link
https://demos.devexpress.com/ASPxEditorsDemos/ASPxComboBox/MultiColumn.aspx
below is my code
<script>
angular.module('selectExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.colors = [
{name:'black', shade:'dark'},
{name:'white', shade:'light'},
{name:'red', shade:'dark'},
{name:'blue', shade:'dark'},
{name:'yellow', shade:'light'}
];
$scope.myColor = $scope.colors[2]; // red
}]);
</script>
<div ng-controller="ExampleController">
<span >
<select ng-model="myColor" ng-options="color.name for color in colors">
<option value="">-- choose color --</option>
</select>
</span>
</div>
You can try with this new directive :
Note: This answer is based on your refrence link.
(function(){
var app = angular.module('selectExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.colors = [
{ name: 'black', shade: 'dark' },
{ name: 'white', shade: 'light' },
{ name: 'red', shade: 'dark' },
{ name: 'blue', shade: 'dark' },
{ name: 'yellow', shade: 'light' }
];
$scope.myColor = $scope.colors[2];
// red
}]);
app.directive('dropdown', function($timeout) {
return {
restrict: 'A',
require: 'ngModel',
scope: {
list: '=dropdown',
ngModel: '=',
selectedKey: '=',
dropHeader: '='
},
templateUrl: '/tpl.html',
replace: true,
link: function(scope, elem, attrs, ngModel) {
scope.head = false;
scope.tdArry = [];
if (scope.dropHeader == true) scope.head = true;
if (Array.isArray(scope.list)) {
var p = scope.list.map(function(_item) {
var keys = Object.keys(_item);
if (keys.length > 0) {
if (scope.tdArry.length == 0) scope.tdArry = keys;
else {
var j = keys.map(function(_k) {
if (scope.tdArry.indexOf(_k) == -1)
scope.tdArry.push(_k);
});
}
return;
} else return;
})
} else {
console.log('Directive Expecting an array of values ')
}
scope.$watch('ngModel', function() {
scope.selected = ngModel.$modelValue;
});
scope.update = function(thing) {
ngModel.$setViewValue(thing);
ngModel.$render();
};
scope.getUpdatedVal = function() {
var selectedKey = scope.selectedKey;
if (selectedKey) {
if (scope.ngModel[selectedKey]) return scope.ngModel[selectedKey];
else {
var k = Object.keys(scope.ngModel);
return scope.ngModel[k[0]];
}
} else {
var k = Object.keys(scope.ngModel);
return scope.ngModel[k[0]]
}
}
},
}
})
})()
.title {
padding: 5px 10px;
border: 1px solid #ccc;
cursor: pointer;
}
.title span {
width: 100%;
display: block;
position: relative;
}
.title span:after {
width: 0;
height: 0;
border: 5px solid transparent;
border-bottom-width: 5px;
border-bottom-style: solid;
border-top-color: transparent;
border-bottom-color: transparent;
border-bottom: none;
border-top-color: #201e1d;
content: '';
vertical-align: middle;
display: inline-block;
position: absolute;
right: 2px;
top: 8px
}
.drop-table {
width: 100%;
border-width: 0;
border-collapse: collapse;
border-spacing: 0;
}
.drop-table th {
background: #eaeef2;
}
.drop-table th,
.drop-table td {
text-align: left;
padding: 5px 7px;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.5/angular.min.js"></script>
<div ng-app="selectExample">
<div ng-controller="ExampleController">
<!-- <div> -->
<div dropdown="colors" drop-header='true' selected-key="'name'" ng-model="myColor"></div>
</div>
<script type="text/ng-template" id="/tpl.html">
<div ng-click="open=!open">
<div class="title">
<span>{{getUpdatedVal()}}</span>
</div>
<table class="drop-table" ng-hide="!open">
<tr ng-if="head">
<th ng-repeat="h in tdArry">{{h}}</th>
</tr>
<tr ng-repeat="li in list" ng-click="update(li)">
<td ng-repeat="d in tdArry">{{li[d]}}</td>
</tr>
</table>
</div>
</script>
</div>
Related
I am building a calculator to help practice learning Vue.js 3 (I am new to vue). I have got the basic functionalities down but I am trying to figure out how to add a hover animation over the buttons. If possible I am trying to make a different hover color between the buttons in white and buttons in orange. Any help would be appreciated
Code:
<div class="calculator">
<div class="display">{{ current || '0'}}</div>
<div #click="clear" class="btn">C</div>
<div #click="sign" class="btn">+/-</div>
<div #click="percent" class="btn">%</div>
<div #click="divide" class="operator">÷</div>
<div #click="append('7')" class="btn">7</div>
<div #click="append('8')" class="btn">8</div>
<div #click="append('9')" class="btn">9</div>
<div #click="multiply" class="operator">x</div>
<div #click="append('4')" class="btn">4</div>
<div #click="append('5')" class="btn">5</div>
<div #click="append('6')" class="btn">6</div>
<div #click="minus" class="operator">-</div>
<div #click="append('1')" class="btn">1</div>
<div #click="append('2')" class="btn">2</div>
<div #click="append('3')" class="btn">3</div>
<div #click="plus" class="operator">+</div>
<div #click="append('0')" class="zero">0</div>
<div #click="dot" class="btn">.</div>
<div #click="equal" class="operator">=</div>
</div>
</template>
<script>
export default {
data() {
return {
previous: null,
current: '',
operator: null,
operatorClicked: false,
hover: false
}
},
methods: {
clear() {
this.current = '';
},
sign() {
this.current = this.current.charAt(0) === '-' ?
this.current.slice(1) : `-${this.current}`;
},
percent() {
this.current = `${parseFloat(this.current) / 100}`;
},
append(number) {
if (this.operatorClicked) {
this.current = '';
this.operatorClicked = false;
}
this.current = `${this.current}${number}`;
},
dot() {
if (this.current.indexOf('.') === -1) {
this.append('.')
}
},
setPrevious() {
this.previous = this.current;
this.operatorClicked = true;
},
plus() {
this.operator = (a,b) => a + b;
this.setPrevious();
},
minus() {
this.operator = (a,b) => a - b;
this.setPrevious();
},
multiply() {
this.operator = (a,b) => a * b;
this.setPrevious();
},
divide() {
this.operator = (a,b) => a / b;
this.setPrevious();
},
equal() {
this.current = `${this.operator(
parseFloat(this.current),
parseFloat(this.previous)
)}`;
this.previous = null;
}
}
}
</script>
<style scoped>
.calculator {
margin: 0 auto;
width: 400px;
font-size: 40px;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: minmax(50px, auto);
}
.display {
grid-column: 1 / 5;
background-color: black;
color: white;
}
.zero {
grid-column: 1 / 3;
border: 1px solid black;
}
.btn {
background-color: white;
border: 1px solid black;
}
.operator {
background-color: orange;
color: white;
border: 1px solid black;
}
</style>
You can use the :hover selector pseudo class, no need to involve js/vue for that
ie:
.btn:hover {
background-color: peach;
}
.operator:hover {
background-color: lavender;
}
Yes, just with hover on btns you can achieve this, no need vue or js
.btn:hover {
background-color: #cac8c3;
}
.operator:hover {
background-color: #6f4d00;
}
Exmaple in this codepen https://codepen.io/JavierSR/pen/LYQdjwY
I am new to development. There of course are a few fundamentals I am struggling with. If anyone could help me and give a little explanation I would greatly appreciate it.
I am creating a synth using the tone.js library. I understand how to create the synth passing it one "Instrument", but I am having an issue doing it dynamically and I am digging myself a hole.
Here's the code:
//instruments
const synth = new Tone.Synth().toDestination();
const amSynth = new Tone.AMSynth().toDestination();
const duoSynth = new Tone.DuoSynth().toDestination();
const fmSynth = new Tone.FMSynth().toDestination();
const membraneSynth = new Tone.MembraneSynth().toDestination();
const metalSynth = new Tone.MetalSynth().toDestination();
const monoSynth = new Tone.MonoSynth({
oscillator: {
type: "square"
},
envelope: {
attack: 0.1
}
}).toDestination();
const noiseSynth = new Tone.NoiseSynth().toDestination();
const pluckSynth = new Tone.PluckSynth().toDestination();
const polySynth = new Tone.PolySynth().toDestination();
const sampler = new Tone.Sampler({
urls: {
A1: "A1.mp3",
A2: "A2.mp3",
},
baseUrl: "https://tonejs.github.io/audio/casio/",
onload: () => {
sampler.triggerAttackRelease(["C1", "E1", "G1", "B1"], 0.5);
}
}).toDestination();
//effects
const distortion = new Tone.Distortion(0.4).toDestination();
const vibrato = new Tone.Vibrato(0.4).toDestination();
const pingPong = new Tone.PingPongDelay("4n", 0.2).toDestination()
const autoWah = new Tone.AutoWah(50, 6, -30).toDestination();
const cheby = new Tone.Chebyshev(50).toDestination();
const autoFilter = new Tone.AutoFilter("4n").toDestination()
const autoPanner = new Tone.AutoPanner("4n").toDestination();
const crusher = new Tone.BitCrusher(4).toDestination();
const chorus = new Tone.Chorus(4, 2.5, 0.5).toDestination();
const feedbackDelay = new Tone.FeedbackDelay("8n", 0.5).toDestination();
const freeverb = new Tone.Freeverb().toDestination();
freeverb.dampening = 1000;
const shift = new Tone.FrequencyShifter(42).toDestination();
const reverb = new Tone.JCReverb(0.4).toDestination();
const phaser = new Tone.Phaser({
frequency: 15,
octaves: 5,
baseFrequency: 1000
}).toDestination();
const tremolo = new Tone.Tremolo(9, 0.75).toDestination();
//array of notes --- we add the sharps in the function
var notes = ['C', 'D', 'E', 'F', 'G', 'A', 'B']
var html = '';
for (var octave = 0; octave < 2; octave++) {
for (var i = 0; i < notes.length; i++) {
var hasSharp = true;
var note = notes[i]
if (note == 'E' || note == 'B')
hasSharp = false;
//white keys with one octive
html += `<div class='whitenote play' onmousedown='noteDown(this)' data-note='${note + (octave + 4)}'>`;
//black keys with one octive
if (hasSharp) {
html += `<div class='blacknote play' onmousedown='noteDown(this)' data-note='${note + '#' + (octave + 4)}'></div>`;
}
html += '</div>'
}
}
$('container').innerHTML = html;
$(".play").click(function(elem) {
var note = elem.dataset.note;
var synth = getSynth(elem.data("synth"));
synth.connect(phaser);
synth.connect(autoWah);
synth.triggerAttackRelease(note, "16n");
event.stopPropagation();
});
function getSynth(name) {
switch (name) {
case "Synth":
return synth;
case "AM":
return amSynth;
case "Duo":
return duoSynth;
case "FM":
return fmSynth;
case "Membrane":
return membraneSynth;
case "Metal":
return metalSynth;
case "Mono":
return monoSynth;
case "Noise":
return noiseSynth;
case "Pluck":
return pluckSynth;
case "Poly":
return polySynth;
}
}
#container {
position: absolute;
height: 200px;
border: 2px solid black;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
white-space: nowrap;
display: block;
white-space: inherit;
overflow: hidden;
}
.whitenote {
height: 100%;
width: 50px;
background: white;
float: left;
border-right: 1px solid black;
position: relative;
}
.blacknote {
position: absolute;
height: 65%;
width: 55%;
z-index: 1;
background: #777;
left: 68%;
}
.instrument-button {
background: orangered;
border: none;
color: white;
padding: 8px 16px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 12px;
}
.effect-button {
background: blue;
border: none;
color: white;
padding: 8px 16px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 12px;
}
.effect-button-padding {
padding-top: 16px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.26/Tone.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.26/Tone.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<script src='node_modules\tone\build\Tone.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.26/Tone.js.map'></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.0.js" integrity="sha256-H+K7U5CnXl1h5ywQfKtSj8PCmoN9aaq30gDh27Xc0jk=" crossorigin="anonymous"></script>
</head>
<body style="align-items: center;">
<div>
<h1>Synth AF</h1>
</div>
<div>
<h4>Intruments</h4>
<button class="btn instrument-button" data-synth="Synth" id="synth-button">Synth</button>
<button class="btn instrument-button" data-synth="AM" id="amSynth-button">AM</button>
<button class="btn instrument-button" data-synth="Duo" id="duoSynth-button">Duo</button>
<button class="btn instrument-button" data-synth="FM" id="fmSynth-button">FM</button>
<button class="btn instrument-button" data-synth="Membrane" id="membraneSynth-button">Drums</button>
<button class="btn instrument-button" data-synth="Metal" id="metalSynth-button">Metal</button>
<button class="btn instrument-button" data-synth="Mono" id="monoSynth-button">Mono</button>
<button class="btn instrument-button" data-synth="Noise" id="noiseSynth-button">Noise</button>
<button class="btn instrument-button" data-synth="Pluck" id="pluckSynth-button">Plucky</button>
<button class="btn instrument-button" data-synth="Poly" id="polySynth-button">Poly</button>
</div>
<div class="effect-button-padding">
<h4>Effects</h4>
<button class='btn effect-button' id="distortion-button">Distortion</button>
<button class='btn effect-button' id="vibrato-button">Vibrato</button>
<button class='btn effect-button' id="pingPong-button">PingPong</button>
<button class='btn effect-button' id="autoWah-button">Wah</button>
<button class='btn effect-button' id="crusher-button">BitCrusher</button>
<button class='btn effect-button' id="phaser-button">Phaser</button>
<button class='btn effect-button' id="reverb-button">Reverb</button>
<button class='btn effect-button' id="autoFilter-button">Auto Filter</button>
<button class='btn effect-button' id="feedbackDelay-button">Feedback</button>
<button class='btn effect-button' id="cheby-button">Chebyshev</button>
</div>
<div class="" id="container">
</div>
</body>
</html>
Here is the UI. (Haven't designed it yet...don't judge)
UI Photo
The idea is to use one of the new instruments created in the one constants by passing it through a button click, to a switch statement and then to the output. I apologize if I didn't explain anything clearly or used the wrong terminology.
I would very much appreciate your help.
Thank you for reading,
FandopTheNoob
// this will be the "state" of the synthesiser
const synthSetup = {
instrument: "",
effects: [],
}
// this is the list of instruments
const instruments = [{
synth: "Synth",
id: "synth-button",
text: "Synth",
tone: new Tone.Synth().toDestination(),
},
{
synth: "AM",
id: "amSynth-button",
text: "AM",
tone: new Tone.AMSynth().toDestination(),
},
]
// this is the list of effects
const effects = [{
id: "distortion-button",
text: "Distortion",
tone: new Tone.Distortion(0.4).toDestination(),
},
{
id: "vibrator-button",
text: "Vibrato",
tone: new Tone.Vibrato(0.4).toDestination(),
},
]
// setting up the instruments & effects buttons
const instrumentsHtml = (instruments) => {
return instruments.map(({
synth,
id,
text
}) => {
return `
<button class="btn instrument-button" data-synth=${synth} id="${id}">${text}</button>
`
}).join('')
}
const effectsHtml = (effects) => {
return effects.map(({
id,
text
}) => {
return `
<button class="btn effect-button" id="${id}">${text}</button>
`
}).join('')
}
const instrumentsContainer = document.getElementById("btn-group-instruments")
instrumentsContainer.innerHTML = instrumentsHtml(instruments)
const effectsContainer = document.getElementById("btn-group-effects")
effectsContainer.innerHTML = effectsHtml(effects)
// setting up click handlers on the instruments & effects buttons
$("body").on("click", ".instrument-button", function() {
// setting up synthSetup.instrument:
synthSetup.instrument = $(this).data("synth")
})
$("body").on("click", ".effect-button", function() {
// setting up synthSetup.effects:
synthSetup.effects = [...new Set([...synthSetup.effects, $(this).attr("id")])]
})
//array of notes --- we add the sharps in the function
var notes = ['C', 'D', 'E', 'F', 'G', 'A', 'B']
var html = '';
for (var octave = 0; octave < 2; octave++) {
for (var i = 0; i < notes.length; i++) {
var hasSharp = true;
var note = notes[i]
if (note == 'E' || note == 'B')
hasSharp = false;
//white keys with one octave
html += `<div class='whitenote play' data-note='${note + (octave + 4)}'>`;
//black keys with one octave
if (hasSharp) {
html += `<div class='blacknote play' data-note='${note + '#' + (octave + 4)}'></div>`;
}
html += '</div>'
}
}
const container = document.getElementById("container")
container.innerHTML = html;
$("body").on("click", ".play", function() {
if (!synthSetup.instrument) {
alert("Choose an instrument first!")
} else {
const {
tone: synth
} = instruments.find(({
synth
}) => synth === synthSetup.instrument)
const note = $(this).data("note")
// connecting the effects
synthSetup.effects.forEach(e => {
const effect = effects.find(({
id
}) => e)
synth.connect(effect.tone)
})
const now = Tone.now()
synth.triggerAttackRelease(note, "2n", now)
}
})
html,
body {
height: 100%;
position: relative;
}
#container {
position: absolute;
height: 200px;
border: 2px solid black;
left: 50%;
bottom: 0;
transform: translateX(-50%);
white-space: nowrap;
display: block;
white-space: inherit;
overflow: hidden;
}
.whitenote {
height: 100%;
width: 50px;
background: white;
float: left;
border-right: 1px solid black;
position: relative;
}
.blacknote {
position: absolute;
height: 65%;
width: 55%;
z-index: 1;
background: #777;
left: 68%;
}
.instrument-button {
background: orangered;
border: none;
color: white;
padding: 8px 16px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 12px;
}
.effect-button {
background: blue;
border: none;
color: white;
padding: 8px 16px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 12px;
}
.effect-button-padding {
padding-top: 16px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.26/Tone.min.js"></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.26/Tone.js.map'></script>
<script src="https://code.jquery.com/jquery-3.6.0.js" integrity="sha256-H+K7U5CnXl1h5ywQfKtSj8PCmoN9aaq30gDh27Xc0jk=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
<div>
<h1>Synth AF</h1>
</div>
<div>
<h4>Intruments</h4>
<div id="btn-group-instruments"></div>
</div>
<div class="effect-button-padding">
<h4>Effects</h4>
<div id="btn-group-effects"></div>
</div>
<div class="" id="container">
</div>
So, here's the next step for this synthesizer:
the current state of the synthesizer is kept in synthSetup
the instruments are stored as an array of objects
the effects are stored as an array of objects
instruments & effects are put to the DOM dynamically
on clicking any white or black button the synthSetup (state) is read & the note is played (chosen instrument & the list of effects)
Issues:
you cannot remove an effect from the list of effects (it could be handled in the click handler of the effect buttons)
I am a beginner to Vue.js. I have 2 components, one as rows and the second one is the sum of all rows above. The problem is that the sums are not updated automatically, even if they are set as computed.
Find below my code :
let s = []
Vue.component('subitem-row', {
props: ['subitem', 'crt', 'si'],
template: `
<tr>
<td>
<div class="form-group" v-if="crt == si">
<label>EPE</label>
<input v-model="subitem.eprice" #change="calculateSubitem();">
</div>
<span v-else>{{subitem.eprice}}</span>
</td>
<td>
<div class="form-group" v-if="crt == si" #change="parseExpresion(); calculateSubitem();">
<label>Anzahl</label>
<input v-model="subitem.qtytext">
</div>
<span v-else>{{subitem.qtytext}}</span>
</td>
<td>
<div class="form-group" v-if="crt == si">
<label>Faktor</label>
<input v-model="subitem.factor" #change="calculateSubitem();">
</div>
<span v-else>{{subitem.factor}}</span>
</td>
<td>
<div class="form-group" v-if="crt == si">
<label>EK</label>
<input v-model="subitem.tp1" readonly>
</div>
<span v-else>{{subitem.tp1}}</span>
</td>
<td>
<div class="form-group" v-if="crt == si">
<label>Marge</label>
<input v-model="subitem.margin" readonly>
</div>
<span v-else>{{subitem.margin}}</span>
</td>
<td>
<div class="form-group" v-if="crt == si">
<label>VK</label>
<input v-model="subitem.tp2" readonly>
</div>
<span v-else>{{subitem.tp2}}</span>
</td>
<td>
<div class="form-group" v-if="crt == si">
<label>Rabatt</label>
<input v-model="subitem.discount" #change="calculateSubitem();">
</div>
<span v-else>{{subitem.discount}}</span>
</td>
</tr>
`,
methods: {
calculateSubitem: function() {
if(this.subitem.hasOwnProperty('eprice') && !isNaN(this.subitem.eprice)) {
if(!this.subitem.hasOwnProperty('factor') || isNaN(this.subitem.factor))
this.subitem.factor = 1
if(!this.subitem.hasOwnProperty('discount') || isNaN(this.subitem.discount))
this.subitem.discount = 0
if(!this.subitem.hasOwnProperty('qty') || isNaN(this.subitem.qty))
this.subitem.qty = 0
let discount = 1 - (parseFloat(this.subitem.discount.toString().split(',').join('.')) / 100),
margin = 0
this.subitem.dprice = (this.subitem.eprice.split(',').join('.') * discount)
this.subitem.tp1 = (this.subitem.dprice * this.subitem.qty * this.subitem.factor)
this.subitem.margin = (this.subitem.tp1 * (parseFloat(margin) / 100))
this.subitem.tp2 = (this.subitem.tp1 + this.subitem.margin)
this.$forceUpdate()//{TODO} - find an alternative to $forceUpdate
}
},
parseExpresion: function() {
this.subitem.qty = parseFloat(this.subitem.qtytext.split(',').join('.')) || 0
this.$nextTick(function () {
this.calculateSubitem()
})
}
},
})
Vue.component('subitem-row-sum', {
props: ['sisum'],
template: `
<tr>
<td colspan="3">SUM</td>
<td>
<span>{{sisum.tp1}}</span>
</td>
<td>
<span>{{sisum.margin}}</span>
</td>
<td>
<span>{{sisum.tp2}}</span>
</td>
<td></td>
</tr>
`,
})
Vue.component('html-textarea',{
template: `<div class="html-textarea" contenteditable="true" #input="updateHTML" rows="3"></div>`,
props: ['value'],
mounted: function () {
this.$el.innerHTML = this.value;
},
methods: {
updateHTML: function(e) {
this.$emit('input', e.target.innerHTML)
}
}
})
const app = new Vue({
el: '#app',
data: {
obj: s, // main object for loading
ii: 0, // items index
si: 0, // subitems index
},
computed: {
items: function() {
return this.obj
},
row: function() {
if(!this.items.length)
this.items.push({})
return this.items[this.ii]
},
subitems: function() {
if(!this.row.hasOwnProperty('subitems'))
this.row.subitems = [{}]
return this.row.subitems
},
srow: function() {
if(!this.subitems.length)
this.subitems.push({})
return this.subitems[this.si]
},
sisum: function() {
let sisum = {
tp1: 0,
tp2: 0,
margin: 0
}
this.subitems.forEach(si => {
sisum.tp1 += si.tp1 || 0
sisum.tp2 += si.tp2 || 0
sisum.margin += si.margin || 0
})
return sisum
}
},
methods: {
setIi: function(i) {
this.ii = i
},
setSi: function(i) {
this.si = i
},
addItemRow: function() {
this.items.push({})
this.setIi(this.items.length - 1)
this.$nextTick(function () {
document.getElementById('items').scrollIntoView({behavior: "smooth", block: "end", inline: "nearest"})
})
},
addSubitemRow: function() {
this.subitems.push({})
this.setSi(this.subitems.length - 1)
this.$nextTick(function () {
document.getElementById('subitems').scrollIntoView({behavior: "smooth", block: "end", inline: "nearest"})
})
},
},
})
body {
padding: 0;
margin: 0;
font-size: 14px;
font-family: 'Courier New', monotype;
}
.tables-container {
display: flex;
flex-direction: column;
height: 100vh;
}
.table-container {
height: calc(50% - 2px);
overflow: auto;
margin: 0 0 30px 0;
background: #eee
}
.table-container td {
padding: 3px 6px;
width: 60px;
height: 30px;
border-bottom: 1px solid black;
vertical-align: middle;
font-size: 1.1rem;
}
.html-textarea-container {
position: relative;
}
.html-textarea {
background: white;
border: 1px solid black;
}
.btn-group-edit {
position: absolute;
right: 2px;
bottom: 2px;
/*display: none;*/
z-index: 1;
}
/*.html-textarea:hover + .btn-group-edit,
.html-textarea:focus + .btn-group-edit {
display: block;
}*/
.table-container .form-group > input,
.table-container .form-group > textarea,
.table-container .form-group > select,
.html-textarea {
width: calc(200px - 12px);
height: calc(30px - 6px);
font-size: 1.1rem;
font-family: 'Courier New', monotype;
word-wrap: break-word;
}
.table-container .form-group > textarea,
.html-textarea {
width: calc(600px - 12px);
}
/*.table-container .form-group > textarea:focus,
.html-textarea {
height: calc(150px - 6px);
}*/
.btn-add {
width: 30px;
height: 30px;
padding: 4px 0;
}
<div id="app">
<div class="tables-container">
<div class="table-container table-subitems">
<table id="subitems">
<tr is="subitem-row" v-for="(subitem, i) in subitems" v-bind:subitem="subitem" v-bind:key="i" v-bind:crt="i" v-bind:si="si" v-on:click.native="setSi(i)"></tr>
<tr is="subitem-row-sum" v-bind:sisum="sisum"></tr>
</table>
</div>
<button class="btn-add" v-on:click="addSubitemRow">+</button>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
Adding numbers to "EPE" and "Anzahl" fields the others are automatically filled, but the sums not. What is my mistake here? Being a computed property, I was thinking that it will be reactive and update itself, based on the current set of subitems.
Thank you.
Welcome to Stack Overflow. I've made an attempt at fixing your code, tbh I found it really difficult to understand and I think you're overusing computed properties, e.g.
items: function() {
return this.obj
},
You might as well just reference this.obj instead of this.items as you're over complicating your code.
Your main problem was initialising the data, Since Vue doesn’t allow dynamically adding root-level reactive properties, you have to initialize Vue instances by declaring all root-level reactive data properties upfront, even with an empty value Declaring Reactive Properties
So I initialised s like this:
let s = [{
subitems: [{
eprice: "0",
factor: 0,
discount: 0,
qty: 0,
dprice: 0,
tp1: 0,
margin: 0,
tp2: 0
}]
}];
let s = [{
subitems: [{
eprice: "0",
factor: 0,
discount: 0,
qty: 0,
dprice: 0,
tp1: 0,
margin: 0,
tp2: 0
}]
}];
Vue.component('subitem-row', {
props: ['subitem', 'crt', 'si'],
template: `
<tr>
<td>
<div class="form-group" v-if="crt == si">
<label>EPE</label>
<input v-model="subitem.eprice" #change="calculateSubitem();">
</div>
<span v-else>{{subitem.eprice}}</span>
</td>
<td>
<div class="form-group" v-if="crt == si" #change="parseExpresion(); calculateSubitem();">
<label>Anzahl</label>
<input v-model="subitem.qtytext">
</div>
<span v-else>{{subitem.qtytext}}</span>
</td>
<td>
<div class="form-group" v-if="crt == si">
<label>Faktor</label>
<input v-model="subitem.factor" #change="calculateSubitem();">
</div>
<span v-else>{{subitem.factor}}</span>
</td>
<td>
<div class="form-group" v-if="crt == si">
<label>EK</label>
<input v-model="subitem.tp1" readonly>
</div>
<span v-else>{{subitem.tp1}}</span>
</td>
<td>
<div class="form-group" v-if="crt == si">
<label>Marge</label>
<input v-model="subitem.margin" readonly>
</div>
<span v-else>{{subitem.margin}}</span>
</td>
<td>
<div class="form-group" v-if="crt == si">
<label>VK</label>
<input v-model="subitem.tp2" readonly>
</div>
<span v-else>{{subitem.tp2}}</span>
</td>
<td>
<div class="form-group" v-if="crt == si">
<label>Rabatt</label>
<input v-model="subitem.discount" #change="calculateSubitem();">
</div>
<span v-else>{{subitem.discount}}</span>
</td>
</tr>
`,
methods: {
calculateSubitem: function() {
if (this.subitem.hasOwnProperty('eprice') && !isNaN(this.subitem.eprice)) {
if (!this.subitem.hasOwnProperty('factor') || isNaN(this.subitem.factor))
this.subitem.factor = 1
if (!this.subitem.hasOwnProperty('discount') || isNaN(this.subitem.discount))
this.subitem.discount = 0
if (!this.subitem.hasOwnProperty('qty') || isNaN(this.subitem.qty))
this.subitem.qty = 0
let discount = 1 - (parseFloat(this.subitem.discount.toString().split(',').join('.')) / 100),
margin = 0
this.subitem.dprice = (this.subitem.eprice.split(',').join('.') * discount)
this.subitem.tp1 = (this.subitem.dprice * this.subitem.qty * this.subitem.factor)
this.subitem.margin = (this.subitem.tp1 * (parseFloat(margin) / 100))
this.subitem.tp2 = (this.subitem.tp1 + this.subitem.margin)
this.$forceUpdate() //{TODO} - find an alternative to $forceUpdate
}
},
parseExpresion: function() {
this.subitem.qty = parseFloat(this.subitem.qtytext.split(',').join('.')) || 0
this.$nextTick(function() {
this.calculateSubitem()
})
}
},
})
Vue.component('subitem-row-sum', {
props: ['sisum'],
template: `
<tr>
<td colspan="3">SUM</td>
<td>
<span>{{sisum.tp1}}</span>
</td>
<td>
<span>{{sisum.margin}}</span>
</td>
<td>
<span>{{sisum.tp2}}</span>
</td>
<td></td>
</tr>
`,
})
Vue.component('html-textarea', {
template: `<div class="html-textarea" contenteditable="true" #input="updateHTML" rows="3"></div>`,
props: ['value'],
mounted: function() {
this.$el.innerHTML = this.value;
},
methods: {
updateHTML: function(e) {
this.$emit('input', e.target.innerHTML)
}
}
})
const app = new Vue({
el: '#app',
data: {
obj: s, // main object for loading
ii: 0, // items index
si: 0, // subitems index
},
computed: {
items: function() {
return this.obj
},
row: function() {
if (!this.items.length)
this.items.push({})
return this.items[this.ii]
},
subitems: function() {
return this.row.subitems
},
srow: function() {
if (!this.subitems.length)
this.subitems.push({})
return this.subitems[this.si]
},
sisum: function() {
debugger;
let sisum = {
tp1: 0,
tp2: 0,
margin: 0
}
this.subitems.forEach(si => {
sisum.tp1 += si.tp1 || 0
sisum.tp2 += si.tp2 || 0
sisum.margin += si.margin || 0
})
return sisum;
}
},
methods: {
setIi: function(i) {
this.ii = i
},
setSi: function(i) {
this.si = i
},
addItemRow: function() {
this.items.push({})
this.setIi(this.items.length - 1)
this.$nextTick(function() {
document.getElementById('items').scrollIntoView({
behavior: "smooth",
block: "end",
inline: "nearest"
})
})
},
addSubitemRow: function() {
this.subitems.push({})
this.setSi(this.subitems.length - 1)
this.$nextTick(function() {
document.getElementById('subitems').scrollIntoView({
behavior: "smooth",
block: "end",
inline: "nearest"
})
})
},
},
})
body {
padding: 0;
margin: 0;
font-size: 14px;
font-family: 'Courier New', monotype;
}
.tables-container {
display: flex;
flex-direction: column;
height: 100vh;
}
.table-container {
height: calc(50% - 2px);
overflow: auto;
margin: 0 0 30px 0;
background: #eee
}
.table-container td {
padding: 3px 6px;
width: 60px;
height: 30px;
border-bottom: 1px solid black;
vertical-align: middle;
font-size: 1.1rem;
}
.html-textarea-container {
position: relative;
}
.html-textarea {
background: white;
border: 1px solid black;
}
.btn-group-edit {
position: absolute;
right: 2px;
bottom: 2px;
/*display: none;*/
z-index: 1;
}
/*.html-textarea:hover + .btn-group-edit,
.html-textarea:focus + .btn-group-edit {
display: block;
}*/
.table-container .form-group>input,
.table-container .form-group>textarea,
.table-container .form-group>select,
.html-textarea {
width: calc(200px - 12px);
height: calc(30px - 6px);
font-size: 1.1rem;
font-family: 'Courier New', monotype;
word-wrap: break-word;
}
.table-container .form-group>textarea,
.html-textarea {
width: calc(600px - 12px);
}
/*.table-container .form-group > textarea:focus,
.html-textarea {
height: calc(150px - 6px);
}*/
.btn-add {
width: 30px;
height: 30px;
padding: 4px 0;
}
<div id="app">
<div class="tables-container">
<div class="table-container table-subitems">
<table id="subitems">
<tr is="subitem-row" v-for="(subitem, i) in subitems" v-bind:subitem="subitem" v-bind:key="i" v-bind:crt="i" v-bind:si="si" v-on:click.native="setSi(i)"></tr>
<tr is="subitem-row-sum" v-bind:sisum="sisum"></tr>
</table>
</div>
<button class="btn-add" v-on:click="addSubitemRow">+</button>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
I am building a Dashboard where you are able to add, delete, move and resize Panels within a grid using GridstackJS. I am filling these Panels with different things. In this Example I am using a Highchart. For saving and restoring the position and Size of the Panels I use the standard serialization of GridstackJS.
My problem now is to save and restore the Content inside the Panels.
Is there an elegant way to save the Content and apply it to the correct Panel when restoring?
JSFiddle
HTML
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/gridstack#0.5.5/dist/gridstack.css" />
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/gridstack#0.5.5/dist/gridstack.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/gridstack#0.5.5/dist/jquery-ui.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/gridstack#0.5.5/dist/gridstack.jQueryUI.js"></script>
<script src="https://code.highcharts.com/highcharts.js"></script>
<button id="save-grid" class="btn btn-primary">Save Grid</button>
<button id="load-grid" class="btn btn-primary">Load Grid</button>
<button id="delete-grid" class="btn btn-primary">Delete Grid</button>
<div class="row">
<div class="col-sm-12">
<div class="grid-stack ui-droppable">
<div class="dragbox grid-stack-item ui-draggable ui-resizable" data-gs-id="draggable">
<h2 class="dragbox-header">Chart 1</h2>
<div class="dragbox-content">
<div class="text-center"> Item 1</div>
</div>
</div>
<div class="dragbox grid-stack-item ui-draggable ui-resizable" data-gs-id="draggable">
<h2 class="dragbox-header">Chart 2</h2>
<div class="dragbox-content"></div>
</div>
<div class="dragbox grid-stack-item ui-draggable ui-resizable" data-gs-id="draggable" data-gs-width="4" data-gs-height="4">
<h2 class="dragbox-header" id="testChartHeader">Chart 3</h2>
<div class="text-center" id="testChart"></div>
</div>
</div>
</div>
</div>
CSS
.dragbox {
margin: 5px 2px 20px;
background: #fff;
position: absolute;
border: 1px solid #ddd;
border-radius: 5px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
}
.dragbox-header {
margin: 0;
font-size: 12px;
padding: 5px;
background: #f0f0f0;
color: #000;
border-bottom: 1px solid #eee;
font-family: Verdana;
cursor: move;
position: relative;
}
.dragbox-content {
display: block;
background: #fff;
margin: 5px;
font-family: 'Lucida Grande', Verdana;
font-size: 0.8em;
line-height: 1.5em;
}
#testChart {
height: 200px;
}
.placeholder {
background: lightgray;
border: 1px dashed #ddd;
border-radius: 5px;
}
JavaScript
$(function() {
var options = {
draggable: {handle: '.dragbox-header', scroll: false, appendTo: 'body'},
placeholderClass: "placeholder",
acceptWidgets: true,
cellHeight: 60
};
$('.grid-stack').gridstack(options);
new function () {
this.serializedData = [
];
this.grid = $('.grid-stack').data('gridstack');
this.loadGrid = function () {
this.grid.removeAll();
var items = GridStackUI.Utils.sort(this.serializedData);
items.forEach(function (node, i) {
this.grid.addWidget($('<div class="grid-stack-item ui-draggable ui-resizable" data-gs-id="draggable"><div class="dragbox grid-stack-item-content ui-draggable-handle"><div class="dragbox-header">Chart ' + (i + 1) + '</div></div></div>'), node);
}, this);
return false;
}.bind(this);
this.saveGrid = function () {
this.serializedData = $('.grid-stack > .grid-stack-item').map(function (i, el) {
el = $(el);
var node = el.data('_gridstack_node');
return {
x: node.x,
y: node.y,
width: node.width,
height: node.height
};
}).toArray();
return false;
}.bind(this);
this.clearGrid = function () {
this.grid.removeAll();
return false;
}.bind(this);
$('#save-grid').click(this.saveGrid);
$('#load-grid').click(this.loadGrid);
$('#delete-grid').click(this.clearGrid);
};
});
var chart = Highcharts.chart('testChart', {
chart: {
animation: false,
type: 'bar'
},
plotOptions: {
series: {
animation: false,
}
},
title: {
text: 'Fruit Consumption'
},
xAxis: {
categories: ['Apples', 'Bananas', 'Oranges']
},
yAxis: {
title: {
text: 'Fruit eaten'
}
},
series: [{
name: 'Jane',
data: [1, 0, 4]
}, {
name: 'John',
data: [5, 7, 3]
}]
});
$('.grid-stack').on('change', function(event, items) {
var chartContainer = chart.renderTo;
$(chartContainer).css(
'height',
$(chartContainer.parentElement).height() - $('#testChartHeader').height()
);
chart.reflow();
});
You need to create a chart again in the loadGrid function:
this.loadGrid = function() {
this.grid.removeAll();
var items = GridStackUI.Utils.sort(this.serializedData);
items.forEach(function(node, i) {
this.grid.addWidget($('<div class="grid-stack-item ui-draggable ui-resizable" data-gs-id="draggable"><div class="dragbox grid-stack-item-content ui-draggable-handle"><div class="dragbox-header">Chart ' + (i + 1) + '</div>' + (i === 2 ? '<div id="testChart"></div>' : '') + '</div></div>'), node);
}, this);
chart = createChart();
return false;
}.bind(this);
Live demo: https://jsfiddle.net/BlackLabel/pjy8b950/
In the code below, I tried creating a simple student grade book with a dynamic div which appears by pressing the show grades button. The button is dynamically created and appears in the table next to the student names. As you can see, the font awesome sorting icons in the table header don't work properly and I don't know where I went wrong with the code. Code snippet:
window.addEventListener('DOMContentLoaded', function () {
console.log('DOM fully loaded and parsed');
"use strict";
// student list
var studentsList = [{
name: "Peter",
grades: [10, 10, 10, 10, 10],
selected: false
},
{
name: "Stark",
grades: [10, 10, 10, 9, 7],
selected: false
},
{
name: "Thanos",
grades: [3, 5, 9, 7, 10],
selected: false
},
{
name: "Hulk",
grades: [2, 2, 2, 2, 10],
selected: false
},
{
name: "Thor",
grades: [10, 5, 9, 4, 10],
selected: false
}
];
// prevent enter from submitting/refreshing
$("#namesInput").keypress(function (event) {
if (event.keyCode == 13) {
event.preventDefault();
$("#addStudentBtn").click();
}
});
//variables
var nameInput = document.getElementById("namesInput");
var addStudentBtn = document.getElementById("addStudentBtn");
var newIndex;
var gradesInput = document.getElementById("gradesInput");
var gradesTableBody = document.getElementById("gradesTableBody");
var addGradesBtn = document.getElementById("addGradesBtn");
var gradesHeader = document.getElementById("gradesHeader");
var oldIndex;
var studentsTableBody = document.getElementById("studentsTableBody");
var gradesWrapper = document.getElementById("gradesWrapper");
var hideGradesBtn = document.getElementById("hideGradesBtn");
var sortGradesDown = document.getElementById("sortGradesDown");
var sortGradesUp = document.getElementById("sortGradesUp");
var sortAverageUp = document.getElementById("sortAverageUp");
var sortAverageDown = document.getElementById("sortAverageDown");
var sortingNaming;
//events
addStudentBtn.addEventListener("click", studentNameInput);
addGradesBtn.addEventListener("click", studentGradesInput);
studentsTableBody.addEventListener("click", studentNameInput);
hideGradesBtn.addEventListener("click", hideGrades);
sortGradesDown.addEventListener("click", studentGradesInput);
sortAverageDown.addEventListener("click", studentNameInput);
sortGradesUp.addEventListener("click", studentGradesInput);
sortAverageUp.addEventListener("click", studentNameInput);
//functions
function drawStudentTable() {
var tableBody = document.getElementById("studentsTableBody");
var tableData = "";
for (var i = 0; i < studentsList.length; i++) {
tableData += `
<tr>
<td></td>
<td>${studentsList[i].name}</td>
<td>${averageGrade(studentsList[i].grades)}</td>
<td><button data-index ="${i}" data-id ="showGrades">Show grades</button></td>
<td></td>
</tr>
`;
}
tableBody.innerHTML = tableData;
}
drawStudentTable(studentsList);
function averageGrade(array) {
var sum = 0;
for (var i = 0; i < array.length; i++) {
sum += parseInt(array[i], 10);
}
var average = (sum / array.length).toFixed(2);
average = Number(average);
return average;
}
function addStudent() {
var newStudent = {
name: nameInput.value,
grades: [],
selected: false,
}
studentsList.push(newStudent);
nameInput.value = "";
}
function selectStudent(event) {
oldIndex = newIndex;
newIndex = Number(event.target.dataset.index);
studentsList[newIndex].selected = true;
if (oldIndex || oldIndex == 0) {
studentsList[oldIndex].selected = false;
}
}
function studentNameInput(event) {
if (nameInput.value && event.target.id == "addStudentBtn") {
addStudent();
drawStudentTable();
} else if (event.target.dataset.id == "showGrades") {
selectStudent(event)
showGrades();
drawGradesTable();
} else if (event.currentTarget.dataset.id == "sortBtn") {
drawStudentTable();
sortingOrder(event);
sortingAverageGrades();
}
}
function addGrade() {
if (gradesInput.value <= 10) {
var newGrade = Number(gradesInput.value);
newGrade = Number(newGrade.toFixed(2));
var grades = studentsList[newIndex].grades;
grades.push(newGrade);
gradesInput.value = "";
} else
alert("Grade is not correctly inserted!");
}
function drawGradesTable() {
var grades = studentsList[newIndex].grades;
gradesTableBody.innerHTML = "";
for (var i = 0; i < grades.length; i++) {
gradesTableBody.innerHTML += `
<tr>
<td>${grades[i]}</td>
</tr>
`;
}
}
function showGrades() {
gradesHeader.innerHTML = studentsList[newIndex].name;
gradesInput.value = "";
gradesWrapper.classList.remove("hidden");
gradesWrapper.classList.add("float");
studentsWrapper.classList.add("float");
}
function hideGrades() {
gradesWrapper.classList.add("hidden");
studentsWrapper.classList.remove("float");
gradesInput.value = "";
}
function studentGradesInput(event) {
if (gradesInput.value && event.target.id == "addGradesBtn") {
addGrade();
drawStudentTable();
drawGradesTable();
} else if (event.target.dataset.id == "showGrades") {
selectStudent(event)
drawGradesTable();
showGrades();
} else if (event.target.id == "hideGradesBtn") {
hideGrades();
} else if (event.currentTarget.dataset.id == "sortBtn") {
drawGradesTable();
sortingOrder(event);
sortingGrades();
}
}
function sortingOrder(event) {
if (event.target.id == "sortGradesDown" || event.target.id == "sortAverageDown") {
sortingNaming = "descending";
} else if (event.target.id == "sortGradesUp" || event.target.id == "sortAverageUp") {
sortingNaming = "ascending";
}
}
function sortingAverageGrades() {
var grades = studentsList[newIndex].grades;
if (sortingNaming == "ascending") {
grades.sort((a, b) => a - b);
} else if (sortingNaming == "descending") {
grades.sort((a, b) => b - a);
}
}
function sortingGrades() {
if (sortingNaming == "ascending") {
studentsList.sort(function (a, b) {
return averageGrade(a.grades) - averageGrade(b.grades);
});
} else if (sortingNaming == "descending") {
studentsList.sort(function (a, b) {
return averageGrade(b.grades) - averageGrade(a.grades);
});
}
}
});
* {
box-sizing: border-box;
margin: 0;
padding: 0;
max-height: 100%;
font-family: 'Dosis', sans-serif;
}
body {
min-width:900px;
height:100%;
}
#studentsWrapper {
text-align: center;
height: 100%;
position: relative;
padding-bottom: 100em;
margin-bottom: -100em;
overflow: hidden;
background-color: lightseagreen;
}
h2 {
font-size: 50px;
margin-bottom: 40px;
margin-top: 30px;
}
td {
height: 40px;
}
table {
text-align: center;
width: 100%;
border-collapse: collapse;
margin-top:50px;
}
th {
text-align: center;
padding:0.3em;
font-size: 25px;
}
.hidden {
display: none;
}
.float {
float: left;
width: 50%;
}
#gradesWrapper {
text-align: center;
height: 100%;
position: relative;
padding-bottom: 100em;
margin-bottom: -100em;
overflow: hidden;
height:100%;
background-color: lightgreen;
}
#hideGradesBtn {
position: absolute;
top: 1em;
left: 1em;
}
input{
border-radius: 50px;
border:none;
height: 30px;
width: 150px;
}
button {
border-radius: 50px;
width: 100px;
border:none;
height: 30px;
}
button:hover {
color:white;
background-color: rgb(27, 145, 139);
border:none;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="description" content="Catalog de note">
<meta name="author" content="Adelina Lipsa">
<meta name="keywords" content="HTML,CSS,JavaScript">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Catalog de note</title>
<link href="catalog.css" rel="stylesheet" type="text/css">
<script src="https://kit.fontawesome.com/2926c10e78.js"></script>
<script src="catalog.js"></script>
</head>
<body>
<div id="studentsWrapper">
<h2><i class="fas fa-user-graduate"></i> Students list </h2>
<div class="input-space">
<input type="text" autocomplete="off" id="namesInput">
<button id="addStudentBtn"><i class="fas fa-plus"></i> Add student</button>
</div>
<table id="table">
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Average grade</th>
<th></th>
<th></th>
</tr>
<tr>
<th></th>
<th></th>
<th><button id="sortAverageDown" data-id="sortBtn" class="sortBtn"><i
class="fas fa-sort-numeric-down"></i></button><button id="sortAverageUp"
data-id="sortBtn" class="sortBtn"><i class="fas fa-sort-numeric-up"></i></button>
</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody id="studentsTableBody"></tbody>
</table>
</div>
<div id="gradesWrapper" class="hidden">
<button id="hideGradesBtn">Hide grades</button>
<h2 id="gradesHeader"></h2>
<div class="grades-space">
<input type="number" id="gradesInput" autocomplete="off" min="1" max="10">
<button id="addGradesBtn">Add grade</button>
</div>
<table>
<thead>
<tr>
<th>Grades</th>
</tr>
<tr>
<th>
<button id="sortGradesDown" data-id="sortBtn" class="sortBtn"><i class="fas fa-sort-numeric-down"></i></button><button id="sortGradesUp" data-id="sortBtn" class="sortBtn"><i class="fas fa-sort-numeric-up"></i></button>
</th>
</tr>
</thead>
<tbody id="gradesTableBody"></tbody>
</table>
</div>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.0/jquery.min.js" type="text/javascript"></script>
<script src="https://ajax.googleapis.com/ajax/libs/d3js/5.9.0/d3.min.js"></script>
</body>
</html>