I'm trying to use some custom UI design styles in my game to display progress bars for various indicators like Exp and hunger, and hopefully even a custom slider tool to adjust some other player attribute but I keep getting issues using $() for my JS expressions.
settings.js:26 Uncaught ReferenceError: $ is not defined
at settings.updateCustomUI (settings.js:26:2)
at endTurnUI (_io.js:701:41)
at io.init (_io.js:1692:3)
at HTMLScriptElement.scriptOnLoad (_settings.js:213:10)
data.js
"use strict"
createItem("me", PLAYER(), {
loc:"lounge",
synonyms:['me', 'myself'],
examine: "Just a regular guy.",
iCurrExp: 0,
iNextLevelExp: 250,
iLevel: 1,
iDifficulty: 90, // Min 1, Max 100 - Slider required
iFullness: 50,
iMetabolism: 10
})
createRoom("lounge", {
desc:"The lounge is boring, the author really needs to put stuff in it.",
})
settings.js
"use strict"
settings.title = "Testing Custom UI"
settings.author = "Your name"
settings.version = "0.1"
settings.thanks = []
settings.warnings = "No warnings have been set for this game."
settings.playMode = "dev"
settings.statusPane = "Status"
settings.statusWidthLeft = 160
settings.statusWidthRight = 0
settings.status = [
function () { return '<td>Level:</td><td style="width: 130px;text-align:left;font-size: 10pt;\">' + w.me.iLevel + '</td>'},
function () { return '<td>Exp:</td><td style="border: thin solid black;background:lightgray;width: 50px;text-align:left;font-size: 8pt;\"><span id="exp-indicator" style="background-color:red;padding-right:80px;paddingBottom:5px;"></span></td>' },
function () { return '<td>Difficulty:</td><td style="border: thin solid black;background:lightgray;width: 50px;text-align:left;font-size: 8pt;\"><span id="difficulty-indicator" style="background-color:red;padding-right:80px;paddingBottom:5px;"></span></td>' },
function () { return '<td>Fullness:</td><td style="border: thin solid black;background:lightgray;width: 50px;text-align:left;font-size: 8pt;\"><span id="fullness-indicator" style="background-color:red;padding-right:50px;paddingBottom:5px;"></span></td>'}
]
settings.customUI = function () {
}
settings.updateCustomUI = function () {
$('#exp-indicator').css('padding-right', w.me.iCurrentExp / w.me.iNextLevelExp);
$('#difficulty-indicator').css('padding-right', w.me.iDifficulty / 100);
$('#fullness-indicator').css('padding-right', w.me.iFullness / 100);
}
I want to figure out why $() doesn't work in settings.updateCustomUI and also how to implement a slider into the mix to adjust difficulty on the fly.
I want to figure out why $() doesn't work in settings.updateCustomUI and also how to implement a slider into the mix to adjust difficulty on the fly.
Are you using jQuery? I thought it wasn't used in the latest QuestJS.
I don't know what is or isn't used with QuestJS, I've only had Quest 5.8 experience. Were there any Git Hub documentation for customizing UI panes in JQuery for QuestJS 1.4?
As far as I can tell, QuestJS doesn't use jQuery. So if you want access to the $
object, you'll have to import it yourself.
However, most of the features that jQuery provided have now been incorporated into core javascript, so for example:
$('#exp-indicator').css('padding-right', w.me.iCurrentExp / w.me.iNextLevelExp);
can probably be replaced by something like:
document.getElementById("exp-indicator").style.paddingRight = w.me.iCurrentExp / w.me.iNextLevelExp;
which doesn't require any additional libraries.
Oh thanks! Are you able to help with an example slider and it's CSS please?
I'd like to implement something like this but I'm unsure how to convert it to using QuestJS's code, can you please help?
https://jqueryui.com/slider/#rangemin
For the difficulty slider, I'd personally just use an input
with type="range"
. You can give it an oninput
attribute to set iDifficulty
whenever the player updates it. It might also make sense to put that in a separate pane since the player will be editing it directly instead of just viewing it, like the status attributes.
It will also probably be helpful for put your CSS in the style.css
file, that way it'll be more organized so you can find it later, and it's also not cluttering up your code. I took a similar approach as you, just changed a few of the width and height values to be more dynamic.
settings.js
settings.status = [
'<td>Level:</td><td id="level-indicator" style="width: 130px;text-align:left;font-size: 10pt;\"></td>',
'<td>Exp:</td><td><div class="stat-background"><div class="stat-bar" id="exp-indicator"></div></div></td>',
'<td>Fullness:</td><td><div class="stat-background"><div class="stat-bar" id="fullness-indicator"></div></div></td>',
]
settings.updateCustomUI = function () {
document.getElementById('level-indicator').innerHTML = w.me.iLevel;
document.getElementById('exp-indicator').style.width = ((w.me.iCurrExp / w.me.iNextLevelExp)) * 100 + "%";
document.getElementById('fullness-indicator').style.width = w.me.iFullness + "%";
}
settings.setup = function()
{
createAdditionalPane(1, "Difficulty", 'difficulty', function() {
let html = ''
html += '<input type="range" min="1" max="100" value="' + w.me.iDifficulty + '" class="slider" id="difficulty-indicator" oninput="w.me.iDifficulty = this.value">'
return html
})
}
style.css
.stat-background {
border: thin solid black;
background-color: lightgray;
width: inherit;
height: inherit;
padding: 3px;
}
.stat-bar {
background-color: red;
height: 5px;
}
#difficulty-indicator {
background-color: lightgray;
accent-color: red;
}
Amazing thank you! I want to be able to lock the minimum of the difficulty slider so they can't lower it beyond a certain value except unless other conditions are applied that change the constant minimum allowed value of the slider, should I just set the min value on it to that and leave max as 100?
So I've added the slider but I'm not getting any visible updates on the difficulty value when I change the slider position, I added a step by 5 value and I know the range is limited to between 70 and 100 with the current value at 90 but it doesn't visibly change value as far as I know. Also my event for Hunger and Fullness doesn't appear to work, I try to wait more than 6 times and nothing happens.
data.js
"use strict"
createItem("me", PLAYER(), {
loc:"lounge",
synonyms:['me', 'myself'],
examine: "Just a regular guy.",
iCurrentExp: 20,
iNextLevelExp: 250,
iLevel: 1,
iDifficulty: 90,
iDifficultyMin: 70,
iFullness: 60,
iHunger: 40,
iHungerRate: 10
})
createItem("EventHunger", {
eventPeriod: 6,
examine: "",
eventActive: true,
eventIsActive: function () {
return true;
},
eventScript: function () {
w.me.iHunger += w.me.iHungerRate
if (w.me.iHunger < 0) {
w.me.iHunger = 0
}
}
})
createItem("EventFullness", {
eventPeriod: 6,
examine: "",
eventActive: true,
eventIsActive: function () {
return w.me.fullness > 0;
},
eventScript: function () {
w.me.iFullness += w.me.iHungerRate
if (w.me.iFullness < 0) {
w.me.iFullness = 0
}
}
})
createRoom("lounge", {
desc:"The lounge is boring, the author really needs to put stuff in it.",
})
settings.js
"use strict"
settings.title = "Testing Custom UI"
settings.author = "Your name"
settings.version = "0.1"
settings.thanks = []
settings.warnings = "No warnings have been set for this game."
settings.playMode = "dev"
settings.statusPane = "Status"
settings.statusWidthLeft = 160
settings.statusWidthRight = 0
settings.setup = function () {
createAdditionalPane(1, "Difficulty", 'difficulty', function () {
let html = ''
html += '<div class="difficulty-slider-container">'
html += '<input type="range" min="' + w.me.iDifficultyMin + '" max="100" step="5" value="' + w.me.iDifficulty + '" class="slider" id="difficulty-slider-indicator" oninput="w.me.iDifficulty = this.value">'
html += '<p>Difficulty : <span id="difficulty--indicator">' + w.me.iDifficulty + '%</span></p><br/>'
html += '</div>'
return html
})
}
settings.status = [
'<td>Level:</td><td id="level-indicator" style="width: 130px;text-align:left;font-size: 10pt;\"></td>',
'<td>Exp:</td><td><div class="stat-background"><div class="stat-bar" id="exp-indicator"></div></div></td>',
'<td>Hunger:</td><td><div class="stat-background"><div class="stat-bar" id="hunger-indicator"></div></div></td>',
'<td>Fullness:</td><td><div class="stat-background"><div class="stat-bar" id="fullness-indicator"></div></div></td>',
]
settings.customUI = function () {
}
settings.updateCustomUI = function () {
document.getElementById('level-indicator').innerHTML = w.me.iLevel;
document.getElementById('exp-indicator').style.width = ((w.me.iCurrentExp / w.me.iNextLevelExp)) * 100 + "%";
document.getElementById('exp-indicator').innerHTML = w.me.iCurrentExp + "/" + w.me.iNextLevelExp;
document.getElementById("hunger-indicator").style.width = ((w.me.iHunger / 100)) * 100 + "%";
document.getElementById("hunger-indicator").innerHTML = w.me.iHunger + "%";
document.getElementById("fullness-indicator").style.width = ((w.me.iFullness / 100)) * 100 + "%";
document.getElementById("fullness-indicator").innerHTML = w.me.iFullness + "%";
}
The way the difficulty slider and iDifficulty
attribute currently interact only allows for data to go from the slider to the attribute, it isn't set up to change visually. If you want data to go from the attribute back to the UI, you need to add in some code into the updateCustomUI
function to make those updates. For every attribute in the UI that can change, you'll need a line updating it, so by my count, you'll need a line for difficulty-slider-indicator.min
, difficulty-slider-indicator.value
and difficulty--indicator.innerHTML
.
As far as your event scripts go, the hunger one looks like it's working to me. The fullness one appears to have a typo in the condition, it should probably be return w.me.iFullness > 0;
. You probably also want to be removing the hunger rate variable from fullness, instead of adding to it.
Whenever something isn't working the way you expect it to, I find it's helpful to add in console.log()
statements to see what the values are. So here, you could log the value of iFullness
in the eventScript, which would show that the script wasn't executing. Likewise, you could log the value of iDifficulty
to see that changing the value on the slider is updating the attribute, it's just not updating the UI element that you created.
I tried to do what you suggested but I couldn't figure out what you meant with the different lines I needed for the updateCustomUI, I always got this error whenever I tried to update the difficulty-indicator span holding my current %.
TypeError: Cannot set properties of null (setting 'innerHTML')
at settings.updateCustomUI (settings.js:46:60)
at endTurnUI (_io.js:701:41)
at io.init (_io.js:1692:3)
at HTMLScriptElement.scriptOnLoad (_settings.js:213:10)
Also when I used the slider I wasn't getting instant visual updates on difficulty-indicator and difficulty-indicator2, I had to use the Wait functionality to see an update occur on both.
"use strict"
settings.title = "Testing Custom UI"
settings.author = "Your name"
settings.version = "0.1"
settings.thanks = []
settings.warnings = "No warnings have been set for this game."
settings.playMode = "dev"
settings.statusPane = "Status"
settings.statusWidthLeft = 160
settings.statusWidthRight = 0
settings.setup = function () {
createAdditionalPane(1, "Difficulty", 'difficulty', function () {
let html = ''
html += '<div class="difficulty-slider-container">'
html += '<input type="range" min="' + w.me.iDifficultyMin + '" max="100" step="5" value="' + w.me.iDifficulty + '" class="slider" id="difficulty-slider-indicator" oninput="w.me.iDifficulty = this.value;">'
html += '<p>Difficulty : <span id="difficulty-indicator"></span></p><br/>'
html += '</div>'
return html
})
}
settings.status = [
'<p><td>Level:</td><td id="level-indicator" style="width: 130px;text-align:left;font-size: 10pt;\"></td></p>',
'<p><td>Exp:</td><td><div class="stat-background"><div class="stat-bar" id="exp-indicator"></div></div></td></p>',
'<p><td>Hunger:</td><td><div class="stat-background"><div class="stat-bar" id="hunger-indicator"></div></div></td></p>',
'<p><td>Fullness:</td><td><div class="stat-background"><div class="stat-bar" id="fullness-indicator"></div></div></td></p>',
'<p><td>Difficulty:</td><td><div class="stat-background"><div class="stat-bar" id="difficulty-indicator2"></div></div></td></p>',
]
settings.customUI = function () {
}
settings.updateCustomUI = function () {
document.getElementById('level-indicator').innerHTML = w.me.iLevel;
document.getElementById('exp-indicator').style.width = ((w.me.iCurrentExp / w.me.iNextLevelExp)) * 100 + "%";
document.getElementById('exp-indicator').innerHTML = w.me.iCurrentExp + "/" + w.me.iNextLevelExp;
document.getElementById("hunger-indicator").style.width = ((w.me.iHunger / 100)) * 100 + "%";
document.getElementById("hunger-indicator").innerHTML = w.me.iHunger + "%";
document.getElementById("fullness-indicator").style.width = ((w.me.iFullness / 100)) * 100 + "%";
document.getElementById("fullness-indicator").innerHTML = w.me.iFullness + "%";
document.getElementById("difficulty-indicator").text = w.me.iDifficulty + "%";
document.getElementById("difficulty-indicator2").style.width = ((w.me.iDifficulty / 100)) * 100 + "%";
document.getElementById("difficulty-indicator2").innerHTML = w.me.iDifficulty + "%";
console.log("Slider value:" + w.me.iDifficulty)
}
Here's how I would go about debugging it. First, look for the first line number in the error, that'll tell you where the error is. If you check line 46 of settings.js
, it's the line where you're setting the text for the difficulty label.
document.getElementById("difficulty-indicator").innerHTML = w.me.iDifficulty + "%";
If we compare that line against the text of the error, it's telling us that we're trying to set null.innerHTML
, which isn't allowed. So let's replace that line with a console log to see what the getElementById
function is returning.
console.log(document.getElementById("difficulty-indicator"))
Now, when we run it, we see that null
is printed in the console, which fits with what we see in the error. But that's weird, because we're creating a span with that id in the settings.setup
function. If you wait one turn, the console log will run again and you'll see that this time, getElementById
returns the span like you expect. So the real problem is that settings.updateCustomUI
is being called before settings.setup
, so we're trying to update that element before it has been created.
Now, how can we resolve this? One way would be to modify settings.updateCustomUI
so that it only tries to update that element if it exists. Another way would be to move your createAdditionalPane
call into the settings.customUI
function so that it runs before settings.updateCustomUI
.
Currently, the only function that is updating your custom elements is settings.updateCustomUI
. Since QuestJS is turn-based, this gets run at the end of every turn, which is why you don't see instant feedback, since the function hasn't run yet. To change it, I would recommend modifying your oninput
attribute of the difficulty-slider-indicator
element. Right now, all it does is set w.me.iDifficulty
to the value of the input, but you could modify it to update the difficulty-indicator
as well.
Okay doing that I get another error when I make the actual change to the value on the slider, also I don't get a display value on the slider at all on first launch of the game.
index.html:69 Uncaught SyntaxError: Unexpected end of input (at index.html:69:64)
settings.customUI = function () {
createAdditionalPane(1, "Difficulty", 'difficulty', function () {
let html = ''
html += '<div class="difficulty-slider-container">'
html += '<input type="range" min="' + w.me.iDifficultyMin + '" max="100" step="5" value="' + w.me.iDifficulty + '" class="slider" id="difficulty-slider-indicator" oninput="w.me.iDifficulty = this.value; document.getElementById("difficulty-indicator").innerHTML = w.me.iDifficulty + "%";">'
html += '<p>Difficulty : <span id="difficulty-indicator"></span></p><br/>'
html += '</div>'
return html
})
}
Also is there a way to set the slider up so that the absolute minimum of 1 is visible but the slider is locked to the custom minimum of 70 (or attribute value) and the user can't move it beyond that point? At the moment all I'm showing is 70-100 range and not 1 to 100.
You've likely got some syntax error with your quotes and the way you're setting the oninput
attribute. Easiest solution would be to create a function that runs everything you want to happen whenever difficulty is updated.
function updateDifficulty(value){
w.me.iDifficulty = value
document.getElementById("difficulty-indicator2").style.width = ((w.me.iDifficulty / 100)) * 100 + "%";
document.getElementById("difficulty-indicator2").innerHTML = w.me.iDifficulty + "%";
document.getElementById("difficulty-indicator").innerHTML = w.me.iDifficulty + "%";
}
Then, change the attribute to be oninput="updateDifficulty(this.value)"
. This has the added benefit that you can call this function if you want to update the difficulty value elsewhere in your code, and it will automatically update the UI.
In fact, this will also help address your other request. You can modify the function to check if the value set in the slider is greater than or equal to your current minimum value. Then, you can hard code the min
of the input to 0, so it will show the full range, but you can't move the slider below the minimum.
function updateDifficulty(value){
if(value >= w.me.iDifficultyMin) w.me.iDifficulty = value
document.getElementById("difficulty-slider-indicator").value = w.me.iDifficulty
document.getElementById("difficulty-indicator2").style.width = ((w.me.iDifficulty / 100)) * 100 + "%";
document.getElementById("difficulty-indicator2").innerHTML = w.me.iDifficulty + "%";
document.getElementById("difficulty-indicator").innerHTML = w.me.iDifficulty + "%";
}
Try to find solutions like this to make your code do what you want it to do. Oftentimes, there won't be a builtin attribute that you can set that fits your precise requirements, so you'll have to build your own.
Awesome this now works, thank you so much!
Two things, first is a query about the timing of the settings events after I put console.log() into them, for some reason the settings.customUI is ran twice and I don't know why:
CustomUI - Difficulty Pane
settings.js:28 CustomUI - Difficulty Pane
settings.js:39 Settings updateCustomUI
settings.js:15 Settings Setup
The last thing, I want is to try and implement the custom handle to the slider so the value is actually on the handle itself instead of a separate indicator to save space like with this example https://jqueryui.com/slider/#custom-handle but its a little difficult figuring out how to convert JQuery from there to QuestJS, any assistance will be greatly appreciated please.
settings.js
"use strict"
settings.title = "Testing Custom UI"
settings.author = "Your name"
settings.version = "0.1"
settings.thanks = []
settings.warnings = "No warnings have been set for this game."
settings.playMode = "dev"
settings.statusPane = "Status"
settings.statusWidthLeft = 160
settings.statusWidthRight = 0
settings.setup = function () {
console.log("Settings Setup")
updateDifficulty(w.me.iDifficulty)
}
settings.status = [
'<p><td>Level:</td><td id="level-indicator" style="width: 130px;text-align:left;font-size: 10pt;\"></td></p>',
'<p><td>Exp:</td><td><div class="stat-background"><div class="stat-bar" id="exp-indicator"></div></div></td></p>',
'<p><td>Hunger:</td><td><div class="stat-background"><div class="stat-bar" id="hunger-indicator"></div></div></td></p>',
'<p><td>Fullness:</td><td><div class="stat-background"><div class="stat-bar" id="fullness-indicator"></div></div></td></p>',
]
settings.customUI = function () {
createAdditionalPane(1, "Difficulty", 'difficulty', function () {
console.log ("CustomUI - Difficulty Pane")
let html = ''
html += '<div class="difficulty-slider-container">'
html += '<div id="custom-handle" class"ui-slide-handle"><input type="range" min="5" max="100" step="5" value="' + w.me.iDifficulty + '" class="slider" id="difficulty-slider-indicator" oninput="updateDifficulty(this.value)"></div>'
html += '<p>Difficulty : <span id="difficulty-indicator"></span></p><br/>'
html += '</div>'
return html
})
}
settings.updateCustomUI = function () {
document.getElementById('level-indicator').innerHTML = w.me.iLevel;
document.getElementById('exp-indicator').style.width = ((w.me.iCurrentExp / w.me.iNextLevelExp)) * 100 + "%";
document.getElementById('exp-indicator').innerHTML = w.me.iCurrentExp + "/" + w.me.iNextLevelExp;
document.getElementById("hunger-indicator").style.width = ((w.me.iHunger / 100)) * 100 + "%";
document.getElementById("hunger-indicator").innerHTML = w.me.iHunger + "%";
document.getElementById("fullness-indicator").style.width = ((w.me.iFullness / 100)) * 100 + "%";
document.getElementById("fullness-indicator").innerHTML = w.me.iFullness + "%";
}
function updateDifficulty(value) {
if (value >= w.me.iDifficultyMin) {
// Increment the difficulty in 5s
var iRemainder = value % 5;
w.me.iDifficulty = value - iRemainder
}
else {
// Enforce the minimum
w.me.iDifficulty = w.me.iDifficultyMin
}
document.getElementById("difficulty-slider-indicator").value = w.me.iDifficulty
document.getElementById("difficulty-indicator").innerHTML = w.me.iDifficulty + "%"
var handle = document.getElementById("custom-handle")
console.log("Handle: " + handle)
handle.text = w.me.iDifficulty + "%"
}
style.css
.stat-background {
border: thin solid black;
background-color: lightgray;
width: inherit;
height: inherit;
padding: 3px;
}
.stat-bar {
background-color: lightcoral;
height: 13px;
}
#custom-handle {
width: 3em;
height: 1.6em;
top: 50%;
margin-top: -.8em;
text-align: center;
line-height: 1.6em;
}
I don't think settings.customUI
is run twice, looking through the code it should only be called once. However, the function you create in createAdditionalPane
does seem to be run at the end of every turn.
settings.customUI = function () {
console.log("customUI")
createAdditionalPane(1, "Difficulty", 'difficulty', function () {
console.log ("Difficulty Pane")
let html = ''...
To your other question, there are a couple of things you could do:
<span id="difficulty-indicator">
element in the updateDifficulty
function so it's centered over the handleBut if all you're trying to do is save some space, the easiest thing to do might be to update the heading of the Difficulty pane in the updateDifficulty
method
document.getElementById("difficulty-side-pane-heading").innerHTML = "Difficulty: " + w.me.iDifficulty + "%"
Okay the last suggestion was what I went with and it looks great so thank you. I did have a query about how to rearrange the order of the displayed panes as I have my Difficulty pane above my Status one and I would rather have the Status always at the top and the custom ones underneath it, how do I control the order in which they are displayed and if I can make some appear or not appear dynamically during gameplay?
Also the Status pane seems to have a default CSS style that I'm not seeing by default on the additional panes so can you please tell me what it is so I can apply it to my custom panes too please? If I used the same table formatting for placement the attributes are not positioned correctly with proper spacing and the span bar width doesn't appear to shrink to the proper available space like the one in the Status pane.
The wiki has some good information about createAdditionalPane
. The first param is a number that controls the order, so in this case, setting it to 2 for the Difficulty pane will place it below the Status pane.
https://github.com/ThePix/QuestJS/wiki/Additional-Side-Pane
Making elements visible or invisible is pretty simple if you can get a reference to the element.
function toggleDifficultyVisibility(){
const difficultyPane = document.getElementById("difficulty-outer")
if (difficultyPane.style.display === "none"){
difficultyPane.style.display = "block"
} else {
difficultyPane.style.display = "none"
}
}
However, it does look like most of the panes don't have id
attributes by default. If you want to toggle the visibility of other panes, you'll either need to assign them ids or find another way to reference them.
For the CSS, you can check this out yourself if you take a look at the Status pane in your dev tools. It should looks something like this
<div class="pane-div">
<h4 class="side-pane-heading" id="status-side-pane-heading">Status</h4>
<table id="status-pane">
<tbody>
<tr>
<td>Level:</td>
<td id="level-indicator" style="width: 130px;text-align:left;font-size: 10pt;">1</td>
</tr>
<tr>
<td>Exp:</td>
<td>
<div class="stat-background">
<div class="stat-bar" id="exp-indicator" style="width: 8%;">20/250</div>
</div>
</td>
</tr>
<tr>
<td>Hunger:</td>
<td>
<div class="stat-background">
<div class="stat-bar" id="hunger-indicator" style="width: 40%;">40%</div>
</div>
</td>
</tr>
<tr>
<td>Fullness:</td>
<td>
<div class="stat-background">
<div class="stat-bar" id="fullness-indicator" style="width: 60%;">60%</div>
</div>
</td>
</tr>
</tbody>
<p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p>
</table>
</div>
The only thing in there I see that we didn't add to style.css
ourselves is status-pane
, which you can find in default.css
#status-pane {
padding-left: 5pt;
}
Okay that all seems fine to me but I get some issues with the progress bars in the additional panes not stretching to the edge of the panel like they do in the Status pane even though I've manually added the same style which at least helped display the contents in a proper table structure like the Status was doing.
createAdditionalPane(1, "Level", "level", function () {
let html = ''
html += '<div class="pane-div">'
html += '<h4 class="side-pane-heading" id="level-side-pane-heading">Level</h4>'
html += '<table id="level-pane">'
html += '<tbody>'
html += '<tr>'
html += '<p><td>Exp: </td><td><div class="stat-background"><div class="stat-bar" id="exp-indicator"></div></div></td></p>'
html += '</tr>'
html += '</tbody>'
html += '</table>'
html += '</div>'
})