I've been building an image uploader that is now theoretically complete, but it is not working in Safari.
I initially thought the problem was because DataTransfer, or more specifically dataTransfer.files was not supported, but this is now supported in Desktop Safari (Version 14.1 +). Although there is no support at all in iOS I will be having the file-picker only (no drag and drop) on iOS so this isn't an issue.
The Problem / Scenario on Desktop Safari
When a user clicks the 'select files' link and submits the images, it all works OK.
When a user drags and drops images the file preview images are duplicated (e.g. 2 images shows 4 previews). When submitted, only the 2 images are uploaded though (which is correct obviously).
When a user clicks the 'select files' link instead of using the drag and drop functionality and then deletes an image from the image previews, the image previews are then duplicated (similar to the way the are in point 2. above).
Logically this would make me think the problem is with the change event listener, but I can't seem to fix it.
Any help would be hugely appreciated.
const dropZone = document.getElementById("drop-zone"),
showSelectedImages = document.getElementById("show-selected-images"),
fileUploader = document.getElementById("standard-upload-files");
dropZone.addEventListener("click", (evt) => {
// assigns the dropzone to the hidden input element so when you click 'select files' it brings up a file picker window
fileUploader.click();
});
// Prevent browser default when draging over
dropZone.addEventListener("dragover", (evt) => {
evt.preventDefault();
});
fileUploader.addEventListener("change", (evt) => {
// this function is further down but declared here and shows a thumbnail of the image
[...fileUploader.files].forEach(updateThumbnail);
});
function getFileListItems(files) {
var transferObject = new ClipboardEvent("").clipboardData || new DataTransfer()
for (var i = 0; i < files.length; i++) transferObject.items.add(files[i])
return transferObject.files;
}
dropZone.addEventListener("drop", (evt) => {
evt.preventDefault();
// assign dropped files to the hidden input element
if (evt.dataTransfer.files.length) {
fileUploader.files = getFileListItems([...fileUploader.files, ...evt.dataTransfer.files]);
}
// function is declared here but written further down
[...evt.dataTransfer.files].forEach(updateThumbnail);
});
// updateThumbnail function that needs to be able to handle multiple files
function updateThumbnail(file) {
if (file.type.startsWith("image/")) {
let uploadImageWrapper = document.createElement("article"),
removeImage = document.createElement("div"),
thumbnailElement = new Image();
// 'x' that deletes the image
removeImage.classList.add("remove-image");
removeImage.innerHTML =
'<svg id="remove-x" viewBox="0 0 150 150"><path fill="#000" d="M147.23,133.89a9.43,9.43,0,1,1-13.33,13.34L75,88.34,16.1,147.23A9.43,9.43,0,1,1,2.76,133.89L61.66,75,2.76,16.09A9.43,9.43,0,0,1,16.1,2.77L75,61.66,133.9,2.77a9.42,9.42,0,1,1,13.33,13.32L88.33,75Z"/></svg>';
// image thumbnail
thumbnailElement.classList.add("drop-zone__thumb");
thumbnailElement.src = URL.createObjectURL(file);
// appending elements
showSelectedImages.append(uploadImageWrapper); // <article> element
uploadImageWrapper.append(removeImage); // 'x' to delete
uploadImageWrapper.append(thumbnailElement); // image thumbnail
// Delete images
removeImage.addEventListener("click", (evt) => {
if (evt.target) {
var deleteImage = removeImage.parentElement;
deleteImage.remove();
fileUploader.files = getFileListItems([...fileUploader.files].filter(f => file !== f));
}
});
}
} // end of 'updateThumbnail' function
body {
margin: 0;
display: flex;
justify-content: center;
width: 100%;
}
form {
width: 30%;
}
#drop-zone {
border: 1px dashed;
width: 100%;
padding: 1rem;
margin-bottom: 1rem;
}
.select-files {
text-decoration: underline;
cursor: pointer;
}
/* images that are previewed prior to form submission*/
.drop-zone__thumb {
width: 200px;
height: auto;
display: block;
}
#remove-x {
width: 1rem;
height: 1rem;
}
#submit-images {
margin: 1rem 0;
}
#show-selected-images {
display: flex;
}
<form id="upload-images-form" enctype="multipart/form-data" method="post">
<h1>Upload Your Images</h1>
<div id="drop-zone" class="drop-zone flex">
<p class="td text-center">DRAG AND DROP IMAGES HERE</p>
<p class="td text-center" style="margin: 0">Or</p>
<p class="tl text-center select-files text-bold pointer">Select Files</p>
</div>
<input id="standard-upload-files" style="display:none" style="min-width: 100%" type="file" name="standard-upload-files[]" multiple>
<input type="submit" name="submit-images" id="submit-images" value="SUBMIT IMAGES">
<div id="show-selected-images"></div>
</form>
The problem is that Safari fires a new (trusted o.O) event when you set the .files property of your <input>. (BUG 180465)
Given they do fire that event synchronously, you could workaround that by using a simple flag:
let ignoreEvent = false; // our flag
const inp = document.querySelector("input");
inp.onchange = (evt) => {
if (ignoreEvent) {
console.log("this event should be ignored");
}
else {
console.log("A true change event");
}
};
const dT = new DataTransfer();
dT.items.add(new File(['foo'], 'programmatically_created.txt'));
// right before setting the .files
ignoreEvent = true;
inp.files = dT.files;
// the change event fired synchronously,
// lower the flag down
ignoreEvent = false;
<input type="file" id="inp">
Which in Safari will output "This event should be ignored".
Now, I can't help but remind that setting the FileList of an <input> like that is still a hack. You really shouldn't use this in production. (I did amend my answer to make it clearer.)
So, please go with a simple Array and a FormData for the upload. Moreover since you don't even show the original file picker <input>.
You apparently already did copy-paste a previous answer that you did not understand, so to avoid you falling in this (comprehensible) trap again, I'll just highlight the key points you will need for your implementation, and I won't give you a working example, on purpose.
So first you need to define an Array ([]) which will store the files your user did select and which is accessible to all of the following functions.
In both the drop and input's change events, you will update this Array with the Files newly selected (you can either append the new Files, or replace them, it's your call).
Then you will add a new event listener to the <form> element's submit event. This event will fire when the submit button is clicked.
Its default action would be to POST the <form>'s content to your server, but since we want to include our own Array and not the <input>'s file list (since it could actually be empty), we don't want this default action to occur, so we will call event.preventDefault() to avoid that.
Now, we still want to send something to the server, so we will build a new form, from scratch.
That's where we'll create a new FormData object, and use its append() method to store our files. The first argument of this method should be the field name, which is also the <input>'s name attribute value. The second one should be a single file, so we'll need to call this method as many times as we have items in our Array. Since we deal with File objects, we don't need the third argument.
Once our FormData is complete, we just have to upload it to the server.
To do so you, in modern browsers you can simply use the fetch() method, passing in the URL to the server, and an option bag looking roughly like
{
method: "POST",
body: formData // the FormData object we just created
}
If you need to support older browsers, you can also do the same with an XMLHttpRequest object, passing the FormData in its .send(formData) method.
Since OP apparently can't get out of this despite the step by step explanations, here is a jsfiddle.
Related
this is written in JS
i cant seem to make the MovieDetails button work at all.
function searchMovie(query) {
const url = `https://imdb8.p.rapidapi.com/auto-complete?q=${query}`;
fetch(url, options)
.then(response => response.json())
.then(data => {
const list = data.d;
list.map((item) => { //makes a list of each individual movie from the data
const name = item.l; // holds the name of movie
const poster = item.i.imageUrl; // holds the poster, given by the data
const detail = item.id // holds the ttid of the movie
// below is what shows the poster, movie name, etc
const movie =
`
<body>
<div class="colmd3">
<div class = "well text-center">
<li><img src="${poster}">
<h2>${name}</h2>
</li>
<button type = "button" id = "MovieDetails" class="btn btn-primary" href="#">Movie Details</button>
<script>
document.getElementById('MovieDetails').addEventListener("click",myFunction);
function myFunction(){
console.log(detail)
}
</script>
</div>
</div>
</body>
`;
document.querySelector('.movies').innerHTML += movie; // returns the first element movies and poster to movie div
//console.log()
});
document.getElementById("errorMessage").innerHTML = "";
})
.catch((error) => {
document.getElementById("errorMessage").innerHTML = error;
});
// we should make a condition here for when a new item is placed here there will be a page refresh
// setTimeout(() => {
// location.reload(); }, 2000);
}
the function above will make an api call and then save the results into list, i can hold specific elements of the list in the three const's and const movie will output the movie poster, name and display it.
I want to make a button for each movie that when clicked will output the id of the movie which is held in const details.
But i cant figure out how to make it work, i have tried (button onclick = function ..) and (document.getElementById...) but it says that getElementById cant be null.
i know that this seems like a silly problem but i cant seem to figure how to make the button actually output something useful or any other way to make a button be mapped out to each api call.
You're heading in the right direction but there are a couple of pain-points with your code as the other commenters have indicated.
Your template string is adding a brand new body element to the page for each movie where there should just be one for the whole document. Nice idea to use a template string though - by far the simplest method to get new HTML on to the page.
Adding JS to the page dynamically like that is going to end up causing you all kinds of problems - probably too many to mention here, so I'll just skip to the good part.
First remove the body element from the template string, and perhaps tidy up the remaining HTML to be a little more semantic. I've used section here but, really, anything other than having lots of divs is a step in the right direction.
Second: event delegation. Element events "bubble up" the DOM. Instead of attaching a listener to every button we can add one listener to the movie list containing element, and have that catch and process events from its children.
(Note: in this example, instead of logging the details to the console, I'm adding the details to the HTML, and then allowing the button to toggle the element on/off.)
// Cache the movie list element, and attach a listener to it
const movieList = document.querySelector('.movielist');
movieList.addEventListener('click', handleClick);
// Demo data
const data=[{name:"movie1",poster:"img1",details:"Details for movie1."},{name:"movie2",poster:"img2",details:"Details for movie2."},{name:"movie3",poster:"img3",details:"Details for movie3."}];
// `map` over the data to produce your HTML using a
// template string as you've done in your code (no body element)
// Make sure you `join` up the array that `map` returns into a
// whole string once the iteration is complete.
const html = data.map(obj => {
return `
<section class="movie">
<header>${obj.name}</header>
<section class="details">${obj.details}</section>
<button type="button">Movie Details</button>
</section>
`;
}).join('');
// Insert that HTML on to the movie list element
movieList.insertAdjacentHTML('beforeend', html);
// This is the handler for the listener attached to the
// movie list. When that element detects an event from a button
// it finds button's previous element sibling (the section
// with the `.details` class), and, in this case, toggles a show
// class on/off
function handleClick(e) {
if (e.target.matches('button')) {
const details = e.target.previousElementSibling;
details.classList.toggle('show');
}
}
.movie { border: 1px solid #555; padding: 0.5em;}
.movie header { text-transform: uppercase; background-color: #efefef; }
.details { display: none; }
.show { display: block; }
button { margin-top: 0.5em; }
<section class="movielist"></section>
Additional documentation
insertAdjacentHTML
matches
classList
toggle
I'm trying to create a website builder (drag and drop page builder) and was wondering where to store the styles when someone changes the styles of an element. For example, in WordPress you can type in your own custom CSS in Customizer (image example: https://i.imgur.com/qaUiVl6.png)
In other page builders like Wix or Google Chrome Inspect Element, you can click button to enable or disable styles.
While making current/live CSS edits to the page, where and how are these styles saved? (I'm not talking about a database as the code has not been saved yet. I'm talking about while making changes onsite changes, where do these "temporary/live" CSS styles get saved?)
You can use the CSSStyleSheet APIs to generate a stylesheet in memory then use insert and delete methods to add or remove rules from the stylesheet at will. When the user is done modifying you could then pass the generated stylesheet back server side to save perm.
Ref docs can be found here:
https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet#Methods
Compatability is IE9+ and all other modern browsers so it has good coverage.
Quick and dirty example below.
var style = (function() {
// Create the <style> tag
var style = document.createElement("style");
// Add the <style> element to the page
document.head.appendChild(style);
return style;
})();
function AddRule(){
//append rule from textbox to ss here
style.sheet.insertRule(document.getElementById("cssIn").value, 0);
document.getElementById("appliedRules").innerHTML = '';
var rules = style.sheet.cssRules;
for (var r in rules) {
if(rules[r].cssText){
document.getElementById("appliedRules").innerHTML += '<br>' + rules[r].cssText;
}
}
}
//enable this to see your special prize in the console
//console.log(style.sheet);
<div class="test"> here we go</div>
Add Rule: <input type="text" id="cssIn" value=".test {color:blue}">
<button type="button" onClick="AddRule();">Add</button>
<div id="appliedRules"></div>
Here is a simple proof-of-concept that demonstrates how this can be done using pure javascript. Just click the save button to see the CSS in the textarea get applied to the page. The CSS is just stored as the input value of the textarea element. You can also make it more complex by using localStorage and an iframe or shadow dom so you only affect a "preview" pane. But this is just a demonstration.
function saveStyles() {
document.querySelector('#style-container').innerHTML = document.querySelector('#style-input').value;
}
#style-input {
width: 100%;
box-sizing: border-box;
display: block;
margin-bottom: 8px;
}
<style id="style-container"></style>
<textarea id="style-input" rows="5">body{background:red;}</textarea>
<button onclick="saveStyles()">Save</button>
Here's an alternative that puts the stylesheet into memory and loads it via a blob URL.
This behaves a bit more like a real stylesheet than inline styles do in some edge cases, which may be desirable in some cases. It can also work on a webpage that blocks inline styles via a Content Security Policy (provided blob URL's are allowed).
(function() {
var styles = document.getElementById('styles');
var save = document.getElementById('save');
var link = null;
function getLink() {
if (!link) {
link = document.createElement('link');
link.rel = 'stylesheet';
document.head.appendChild(link);
}
return link;
}
save.addEventListener('click', function() {
var link = getLink();
if (link.href) {
URL.revokeObjectURL(link.href);
}
link.href = URL.createObjectURL(new Blob([styles.value], {type: 'text/css'}));
});
})();
#styles {
display: block;
width: 95%;
}
<textarea id="styles" rows="5">body {
background: green;
}
</textarea>
<button id="save">Save</button>
The answers here focus on the methods for building a stylesheet and adding css rules using common methods browsers provide as part of the DOM api. These are the underlying function calls that any UI framework on the web will use.
But when you ask, "Where is this stored?". In a sense, you are asking how is the "state" managed. If you look at original post jQuery/web app building frameworks, like Backbone.js -- its motto was, "Get your model out of the DOM". So generally the "elements" of the ui-building tools will themselves be represented as component/models.
If you look at view-centric frameworks, like React or Vue, more complex web apps will use a framework like Redux to handle "state", which is stored in single object store. This represents the current options of all the components on the page.
So ultimately, a non-toy WYSIWYG web editor will likely treat each "element" as a component that renders its styles based on an inputted state.
This coupled with a controlled and predictable way to change state, allows for the managing of complex UI. For instance, the click method would trigger an action that acts like the name of an event handler, triggering the functions (in redux world, reducers), which ultimately changes the state, in our example, color of the element.
The low-level calls to the DOM that facilitate this in a complex web-app/web-editor would be abstracted away.
Based on the discussion, I can suggest to use separate styles for each element id. Here is a sketch.
<script>
function setStyle(id, style_text)
{
var style_id = "style_" + id;
var style_forId = "#" + id + " " + style_text;
var styleDom = document.getElementById(style_id);
if(!styleDom)
{
styleDom = document.createElement('style');
styleDom.type = 'text/css';
styleDom.id = style_id;
styleDom.innerHTML = style_forId;
document.getElementsByTagName("head")[0].appendChild(styleDom);
}
else
{
styleDom.innerHTML = style_forId;
}
}
</script>
<button id="myButton1" type="button" >My Button 1</button>
<button id="myButton2" type="button" >My Button 2</button>
<br>
<button onclick="setStyle('myButton1', '{color:red}'); "> Set Red color for myButton1 </button>
<br>
<button onclick="setStyle('myButton2', '{color:red}'); "> Set Red color for myButton2 </button>
<br>
<button onclick="setStyle('myButton1', '{color:green}'); "> Set Green color for myButton1 </button>
<br>
<button onclick="setStyle('myButton2', '{color:green}'); "> Set Green color for myButton2 </button>
<br>
Great Answers already just putting a different viewpoint out there.
Simple Version
Using CSSStyleSheet
const style = document.createElement("style");
document.head.appendChild(style);
style.sheet.insertRule(`
header {
background: 'black'
}
`, 0);
Real World
I would take this simple idea and control it through a data structure like this.
// A JS Object to control and modify CSS styles on.
const css = {
header: {
background: 'black',
border: '2px green solid',
'font-size': '12px'
}
}
// Converts JS Object to CSS Text (This is not battle tested)
function objToCss(style) {
return Object.entries(style).reduce((styleString, [propName, propValue]) => {
propName = propName.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);
if (typeof propValue === 'object') {
return `${styleString}${propName}{${objToCss(propValue)}}`;
} else {
return `${styleString}${propName}:${propValue};`;
}
}, '')
}
// Setup
const style = document.createElement("style");
document.head.appendChild(style);
style.sheet.insertRule(objToCss(css), 0);
// Updates
css.header.border = '1px blue solid';
style.sheet.replace(objToCss(css), 0);
I am trying to allow users to upload an image by using the html below
<input class="file-upload" id="imagepath" name="imagepath" type="file" accept="image/x-png,image/jpeg" />
Is there any way to show a validation message if a user selects a file that is not of the correct type using JS/JQuery?
I understand that the 'accept' attribute restricts what a user will see when they are viewing the file explorer however this can be changed using the drop down, to 'All files'.
You can fetch the extension of an uploaded file.
var files = event.target.files;
var filename = files[0].name;
var extension = files[0].type;
And apply the check and allow whichever file type you wanted to allow. Say for images stuff.
if(extension == "jpeg" || extension == "jpg") {
//do you code
} else {
//display an error
}
I think J D already gave you a solid answer on how to check the file type.
I looked around and found an example on MDN which I modified a bit. It exists of a list of accepted file types, like the one in your accept attribute and a function which will check if the type of a file matches the ones in your accept attribute.
The isValidFileType accepts a File and and array as arguments which then will check if the File.type is present in the types array. If it is it will return true and not it will return false.
Same principles as J D described but with a functional programming approach.
// The input element.
const imagePath = document.getElementById('imagepath');
// List of accepted file types, gotten from your input's accept property.
const acceptedFileTypes = imagePath.accept.split(','); // ['image/jpeg', 'image/x-png']
// Check if file has the correct type.
const isValidFileType = (file, types) => types.some(type => type === file.type);
So on your form element you should attach a change event listener. And in that listener loop over all of the files and check if they have the correct extension.
imagePath.addEventListener('change', (event) => {
// Check if every file has the accepted extension.
const filesAccepted = event.target.files.every(file =>
isValidFileType(file, acceptedFiles)
);
if (filesAccepted) {
// Files are accepted
} else {
// Files are not accepted.
}
});
Although J D's example is a lot more compact and to the point, this example provides reusability and prevents you from writing the same code twice, or more. Also extending the list of accepted types is as easy as adding them to the accept attribute of the HTML element.
Thanks For the help guys,
This is the solution I went with in the end and works perfectly
<!--HTML-->
<div id="fileErrorMessage" class="hide" >file type is not suuported</div>
<input class="some-class" id="imagepath" name="imagepath" type="file" accept="image/x-png,image/jpeg" />
/*css*/
#fileErrorMessage {
color: red;
font-weight: bold;
}
.hide {
display: none;
}
//JavaScript
$('#imagepath').on('change', function (event) {
var files = event.target.files;
Array.from(files).forEach(x => {
if (!x.type.includes("jpeg") && !x.type.includes("jpg") && !x.type.includes("png")) {
$("#fileErrorMessage").removeClass("hide");
$('#imagepath').val('');
}
else if (!$("#fileErrorMessage").hasClass("hide")) {
$("#fileErrorMessage").addClass("hide");
}
});
});
I'm looking for a way to make html5 audio play as soon as button is clicked. Right now, it does work; but not very reliably.
My file is a 300Kb MP3 (128Kbps) that is hosted on AWS Cloudfront.
<audio id="i-audio" src="https://cdn/url/to/i-audio.mp3" preload="auto" type="audio/mp3"></audio>
this.iAudio = document.getElementById('i-audio');
$("#button").on('click', function() {
this.iAuido.play();
});
I learn that play() will return a promise; and I'm not sure how can I be sure that audio plays all the time; reliably.
Right now, the play would work at random times or just throw an error.
If you're getting error messages about Promises and .play(), you'll need to treat .play() as a Promise, see this article.
I didn't see any button#button so I'm assuming that's just a typo.
this has no context so it'll be referencing window (very useless)
this.iAudio = document.getElementById('i-audio');
A variable (like var iAudio = ... or const iAudio = ...) should've been declared.
this has context of the element listening to the click event
$("#button").on('click', function() {
this.iAuido.play();
});
Can't imagine that this code worked even randomly it just gets worse as I read the next line. Let's assume iAudio is correct and references the <audio> tag, but for some reason this is prefixed to it (wHy?!?). this would reference the button so every segment of a line of code is all sorts of wrong.
const playback = (e) => {
const mp3 = $('.mp3')[0];
let action = mp3.paused || mp3.ended ? mp3.play() : mp3.pause();
return action;
}
playback().then(() => console.log('Promise Resolved')).catch(error => $('.btn').on('click', playback));
button {
font-size: 5rem;
color: cyan;
border: 0;
background: none;
cursor: pointer
}
button:focus {
outline: none
}
button:hover {
color: tomato;
}
<audio class="mp3" src="https://glsbx.s3.amazonaws.com/-/dd.mp3" preload="auto"></audio>
<button class='btn' type='button'>▶</button>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
I've been trying to implement History.js. I've got some understanding of how getting and pushing states work, however I'm having particular trouble with the data storing component of the history along with using global variables.
As a simple example, I decided to try and set up a script which would change the colour of a html box upon being clicked. This would also trigger the history - essentially creating a history for clicking the box (and its colour being changed on each state of the history).
Is there any way to update a global variable based on the data (in this case, updating i per click) supplied in History State's data?
HTML:
<div id="box">CLICK ME</div>
<button id="back">Back</button>
<button id="forward">Forward</button>
CSS:
#box {
width: 300px;
height: 300px;
background-color: black;
color: white;
vertical-align: middle;
text-align: center;
display: table-cell;
}
JavaScript:
var History = window.History;
var i = 0;
if (History.enabled) {
var State = History.getState();
History.pushState({count:i}, $("title").text(), State.urlPath);
} else {
return false;
}
// Bind to StateChange Event
History.Adapter.bind(window,'statechange', function(){
State = History.getState();
console.log(State.data, State.title, State.url);
$(this).css('background-color', getColour());
});
// Trigger the change
$("#div").on("click", function() {
i++;
History.pushState({count:i},"State " + i,"?state=" + i);
});
function getColour() {
var colours = ["red", "orange", "yellow", "green", "aqua","blue", "purple", "magenta","black"];
if (i > colours.length - 1) {
i = 0;
}
if (i < 0) {
i = colours.length - 1;
}
return colours[i];
}
$("#back").on("click", function() {
History.back();
});
$("#forward").on("click", function() {
History.forward();
});
I'm also using JQuery, ajaxify-html5.js and scrollto.js as per recommendation by other threads.
Editable JSFiddle | Viewable JSFiddle
After playing around with this a ton (and reading more questions), I've figured it out. I'll detail what the solution means to any others who come across this.
JSFIDDLE VIEW SOLUTION | JSFIDDLE VIEW SOLUTION
First here's the final code. Note that the JavaScript has document.ready extras to get it working outside of JSFiddle.
It's also worth noting I took out ajaxify-html5.js and scrollto.js out, as they weren't needed (and were breaking the code somewhere).
HTML:
<div id="box">
<div id="count"></div>
<div id="colour"></div>
</div>
<button id="back">Back</button>
<button id="forward">Forward</button>
CSS:
#box {
width: 300px;
height: 300px;
background-color: white;
color: white;
vertical-align: middle;
text-align: center;
display: table-cell;
}
button {
width: 148px;
height: 40px;
}
#count, #colour {
background-color: black;
font-family: "Consolas";
}
JavaScript:
var History = window.History;
var i = 0;
var colour = getColour();
var colourName = getColourName();
$(document).ready(function() {
if (History.enabled) {
changeHistory();
}
else {
return false;
}
// Bind to StateChange Event
History.Adapter.bind(window,'statechange', function(){
State = History.getState();
i = State.data.count;
colour = State.data.colour;
colourName = State.data.colourName;
changeHistory();
});
// Trigger the change
$(document).on("click", "#box", function() {
i = i + 1;
colour = getColour();
colourName = getColourName();
changeHistory();
});
$(document).on("click", "#back", function() {
History.back();
});
$(document).on("click", "#forward", function() {
History.forward();
});
});
function getColour() {
var colours = ["rgb(220,45,45)", "orange", "rgb(230,230,50)", "rgb(15,210,80)", "rgb(100,220,220)","rgb(50,80,210)", "rgb(140,20,180)", "rgb(230,70,110)","grey"];
if (i > colours.length - 1) {
i = 0;
}
if (i < 0) {
i = colours.length - 1;
}
return colours[i];
}
function getColourName() {
var colourNames = ["Red","Orange","Yellow","Green","Light Blue","Blue","Purle","Pink","Grey"];
return colourNames[i];
}
// Make the changes
function changeHistory () {
$("#colour").html(colourName);
$("#count").html(i);
$("#box").css('background-color', colour);
History.pushState({count:i, colour: colour, colourName: colourName},"A Shade of " + colourName,"?colour=" + colourName);
}
So going back to what I wanted to achieve with the question:
Clicking the box would add history
Each history would hold variables required to affect global variables
Its worth noting the solution specifically uses variables from each iteration of the history to power the global variables, whereas the program itself uses the global variables. The variables used to power the interface never access the ones stored in history.
Let's break up the program into separate and simpler processes and functions. Much like other history.js solutions, there's things you require to get it working:
History.getState(): Gets the latest history item "from the stack"
History.Adapter.bind(window,'statechange', function() {}: An event listener which will trigger a function when the window has a statechange (history change in this case)
History.pushState({data}, title, url): Pushes a state into the "history stack". Holds an object (data), title (of the tab/window) and url to display.
Setting the history:
When a user clicks on the box, the program should:
increment the counter (i)
change the colour and colourName
add the new history stack object in
I decided to separate the first two features from the last one. The function changeHistory() is responsible for updating the contents of the box (from global variables) and pushing a new history object in (using global variables).
changeHistory() gets called whenever I want to add a new item of history in and update the contents in the box to reflect the new history - so at launch at when the box is clicked.
When the box is clicked, the first two criteria get met. Using the existing global variables and functions, the new colour and name are retrieved and set as the global variables.
This is how it should behave:
Box Click -> Increment i, Change variables -> Push History
Listen for the history change:
Once a history change has been made (either by clicking the box, pressing back/forward buttons or browser buttons), a change needs to occur.
By creating the variable State = History.getState(), we have an easy way of accessing the data from the latest history stack object.
We'll use this data from the history object.data to assign to the global variables. After updating the variables, we'll update the view using changeHistory().
This is how the model should work:
History changed -> Update globals from history -> Update View
History change will occur whenever someone presses back, forwards or the box, accounting for all possible changes.