Intended Goal - User selects different colors from various color inputs and creates their own theme. Once the colors are chosen, the user clicks the download button and gets the generated CSS file with the colors he/she chose.
Issue - I'm able to download the CSS file, but I'm getting the original values despite changing the inputs to different colors.
What I've Done
The CSS file that's being downloaded already exists and all of the colors that correspond to different elements are done via CSS variables.
I'm updating the changes live by doing the following.
import { colors } from './colorHelper'
const inputs = [].slice.call(document.querySelectorAll('input[type="color"]'));
const handleThemeUpdate = (colors) => {
const root = document.querySelector(':root');
const keys = Object.keys(colors);
keys.forEach(key => {
root.style.setProperty(key, colors[key]);
});
}
inputs.forEach((input) => {
input.addEventListener('change', (e) => {
e.preventDefault();
const cssPropName = `--${e.target.id}`;
document.styleSheets[2].cssRules[3].style.setProperty(cssPropName, e.target.value);
handleThemeUpdate({
[cssPropName]: e.target.value
});
console.log(`${cssPropName} is now ${e.target.value}`)
});
});
Then, I fetched the stylesheet from the server, grabbed all the CSS Variables and replaced them with their actual value (hex color value).
After that, I got the return value (new stylesheet without variables) and set it for the data URI.
async function updatedStylesheet() {
const res = await fetch("./prism.css");
const orig_css = await res.text();
let updated_css = orig_css;
const regexp = /(?:var\(--)[a-zA-z\-]*(?:\))/g;
let cssVars = orig_css.matchAll(regexp);
cssVars = Array.from(cssVars).flat();
for (const v of cssVars) {
updated_css = updated_css.replace(v, colors[v.slice(6, -1)]);
};
return updated_css;
}
const newStylesheet = updatedStylesheet().then(css => {
downloadBtn.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(css));
downloadBtn.setAttribute('download', 'prism-theme.css');
})
I already have a download button setup in my HTML and I grabbed it earlier in my script so that it was available anywhere for me to use. downloadBtn
I set up the button to fire and grabbed the new sheet.
downloadBtn.addEventListener('click', () => {
newStylesheet();
});
The Result
I get the initial color values of the stylesheet despite changing the colors within the color inputs on the page. So the CSS file isn't being updated with the new colors before I download the file.
You could use PHP to pass the values to a new page. Let's say you chose the colors you want then click a button that says "Generate" that takes you to the "Generate Page".
The values would be passed directly into the HTML and you would download from the Generate Page instead.
This is if you know PHP of course, just a suggestion on how you might solve it.
(would comment, but can't due to reputation)
Related
I'm making a website and on one of my pages I have this button that when clicked, should take a haiku from a JSON file that contains over 5000 and display it to the user. Currently I do this the following way.
<script>
const haikuUrl = 'http://mywebsite/haikus.json';
async function getHaiku() {
const num = Math.floor(Math.random() * 5145);
const response = await fetch(haikuUrl);
const data = await response.json();
const {line1, line2, line3} = data[num];
document.getElementById('line1').textContent = line1;
document.getElementById('line2').textContent = line2;
document.getElementById('line3').textContent = line3;
}
document.getElementById('haikuButton').addEventListener('click', () => {
getHaiku();
});
</script>
I'm pretty much new to JS, and after looking around and watching some videos this was the only way I could get this to work but I'm pretty much sure there has to be a better way to do this. Right now, I'm having to load the whole file every time just to get 1 random object from the JSON. What I'd like to have is a constant or a variable that just sits there waiting for the user to ask for a random one. Something like this:
<script>
const data= [{},{},{}...{}]; //an array of all the haikus loaded once at the beginning
function getHaiku() {
const num = Math.floor(Math.random() * 5145);
const {line1, line2, line3} = data[num];
document.getElementById('line1').textContent = line1;
document.getElementById('line2').textContent = line2;
document.getElementById('line3').textContent = line3;
}
document.getElementById('haikuButton').addEventListener('click', () => {
getHaiku();
});
</script>
So pretty much the same just without the having to fetch the whole data every time.
I guess one option could be to hardcode the whole dataset into the js file into a variable, but something tells me there's got to be a better way.
I would first fetch the data, then choose a random haiku by using the randomly generated number as an index of the data array. Something like below:
I have not tested the code below so I am not sure if it works, but hopefully this nudges you in the right direction.
let data;
let button = document.getElementById('haikuButton');
let randomHaiku = '';
// Fetch data
async function getHaikus(){
const response = await fetch('http://mywebsite/haikus.json')
data = await response.json();
}
// Generate random number
function generateRandomNumber(array){
return Math.floor(Math.random() * array.length)
}
// get random haiku
button.addEventListener('click', ()=>{
let index = generateRandomNumber(data)
randomHaiku = data[index]
console.log(randomHaiku);
}, false)
getHaikus();
If I understand correctly your question, one optimization you can do is as follows:
const haikuUrl = 'http://mywebsite/haikus.json';
let haikus = null;
async function getHaiku() {
const num = Math.floor(Math.random() * 5145);
if (!haikus) {
const response = await fetch(haikuUrl);
haikus = await response.json();
}
const {line1, line2, line3} = haikus[num];
[...]
}
So it would download the file the first time the user clicks on the button, but not download it again if the user clicks on the button again (unless they close the webpage in between).
You should also definitely use the length of the array instead of hardcoding 5145, as mentioned in other answers.
It is also certainly possible to embed the data directly in the JS file, but it won't make much difference, it will just make the JS file (which must be downloaded as well) much bigger. So it wouldn't solve the problem of needing to download all the haikus when you need just one.
Making it so that it really only downloads one haiku is a bit more complicated. You could have a backend that the frontend requests a single haiku to (but that probably increases complexity significantly if you currently don't have a backend). You could also store all haikus in separate files with predictable names (for instance haikus/42.txt) and fetch only a single such file.
I'm trying to fill pdf form usinf PDFLib js.
In my localhost it works fine but after deployment "field.constructor.name" return "t" type. and the data is no filled.
there is no error, but nothing happened.
const pdfDoc = await PDFDocument.load(formPdfBytes);
// Register the `fontkit` instance
pdfDoc.registerFontkit(fontkit);
// Embed our custom font in the document
const customFont = await pdfDoc.embedFont(fontBytes, { subset: true });
// Get the form containing all the fields
const form = pdfDoc.getForm();
const fields = form.getFields()
fields.forEach(field => {
//HERE THE PROBLEM!!
const type = field.constructor.name // GOT "t"
const name = field.getName();
I found the problem. The font file was problematic.
i'm working on a simple note-taking app for my portfolio using JS and Firebase. Before i tell you what's happening i feel like i need to show you how my code works, if you have any tips and concerns please tell me as it would be GREATLY appreciated. That being said, let's have a look "together". I'm using this class to create the notes:
const htmlElements = [document.querySelector('.notes'), document.querySelector('.note')];
const [notesDiv, noteDiv] = htmlElements;
class CreateNote {
constructor(title, body) {
this.title = title;
this.body = body;
this.render = () => {
const div1 = document.createElement('div');
div1.className = 'notes-prev-container';
div1.addEventListener('click', () => { this.clickHandler(this) });
const div2 = document.createElement('div');
div2.className = 'notes-prev';
const hr = document.createElement('hr');
hr.className = 'notes__line';
// Nest 'div2' inside 'div1'
div1.appendChild(div2);
div1.appendChild(hr);
/*
Create Paragraph 1 & 2 and give them the same
class name and some text
*/
const p1 = document.createElement('p');
p1.className = 'notes-prev__title';
p1.innerText = this.title;
const p2 = document.createElement('p');
p2.className = 'notes-prev__body';
p2.innerText = this.body;
// Nest p 1 & 2 inside 'div2'
div2.appendChild(p1);
div2.appendChild(p2);
// Finally, render the div to its root tag
notesDiv.appendChild(div1);
}
}
/*
Every time this method is called, it creates 2 textareas,
one for the note title and the other for its body then it
appends it to the DOM.
*/
renderNoteContent () {
const title = document.createElement('textarea');
title.placeholder = 'Title';
title.value = this.title;
title.className = 'note__title';
const body = document.createElement('textarea');
body.placeholder = 'Body';
body.value = this.body;
body.className = 'note__body';
noteDiv.appendChild(title);
noteDiv.appendChild(body);
}
/*
When this method is called, it checks to see if there's a
note rendered already (childElementCount === 1 because there's a
button, so if there's only this button it means there's no
textareas rendered).
If yes, then merely call the renderNoteContent method. Else
get the tags with the classes 'note__title' and 'note__body'
and remove them from the DOM, then call renderNoteContent to
create the textareas with the clicked notes values.
This function gets mentioned at line 19.
*/
clickHandler(thisClass) {
if (noteDiv.childElementCount === 1) {
thisClass.renderNoteContent();
} else {
document.querySelector('.note__title').remove();
document.querySelector('.note__body').remove();
thisClass.renderNoteContent();
}
}
}
Now i need 2 buttons, createNotesButton and saveNotesButton respectively. These 2 buttons must be inside a function that will be called inside .onAuthStateChanged (why? because they will be needing access to the currentUser on firebase auth).
I want the createNotesButton to create a note prototype, render it to the DOM and create a new document on firestore, where this note contents will be stored. Here's how i did it:
PS: I feel like i'm not using this class correctly, so again if you have any tips i appreciate it.
import {db} from '../../firebase_variables/firebase-variables.js';
import {CreateNote} from '../create_notes_class/create_notes_class.js';
const htmlElements = [
document.querySelector('.createNotes-button'),
document.querySelector('.saveNotes-button')
];
const [createNotesButton, saveNotesButton] = htmlElements;
function clickHandler(user) {
/*
1. Creates a class.
2. Creates a new document on firebase with the class's empty value.
3. Renders the empty class to the DOM.
*/
createNotesButton.addEventListener('click', () => {
const note = new CreateNote('', '');
note.render();
// Each user has it's own note collection, said collection has their `uid` as name.
db.collection(`${user.uid}`).doc().set({
title: `${note.title}`,
body: `${note.body}`
})
})
}
Now i need a saveNotesButton, he's the one i'm having issues with. He needs to save the displayed note's content on firestore. Here's what i tried doing:
import {db} from '../../firebase_variables/firebase-variables.js';
import {CreateNote} from '../create_notes_class/create_notes_class.js';
const htmlElements = [
document.querySelector('.createNotes-button'),
document.querySelector('.saveNotes-button')
];
const [createNotesButton, saveNotesButton] = htmlElements;
function clickHandler(user) {
createNotesButton.addEventListener('click', () => {...})
/*
1. Creates 2 variables, `title` and `body, if there's not a note being displayed
their values will be null, which is why the rest of the code is inside an if
statement
2. If statement to check if there's a note being displayed, if yes then:
1. Call the user's note collection. Any document who has the title field equal to the
displayed note's value gets returned as a promise.
2. Then call an specific user document and update the fields `title` and `body` with
the displayed note's values.
3. If no then do nothing.
*/
saveNotesButton.addEventListener('click', () => {
const title = document.querySelector('.note__title');
const body = document.querySelector('.note__body');
db.collection(`${user.uid}`).where('title', '==', `${title.value}`)
.get()
.then(userCollection => {
db.collection(`${user.uid}`).doc(`${userCollection.docs[0].id}`).update({
title: `${title.value}`,
body: `${body.value}`
})
})
.catch(error => {
console.log('Error getting documents: ', error);
});
});
}
This didn't work because i'm using title.value as a query, so if i change it's value it will also change the queries direction to a path that doesn't exist.
So here's the question: how can i make it so the saveNotesButton does its job? I was thinking of adding another field to each note, something that won't change so i can easily identify and edit each note. Again, if there's something in my code that you think can or should be formatted please let me know, i'm using this project as a way to solidify my native JS knowledge so please be patient. I feel like if i had used React i would've finished this sometime ago but definitely wouldn't have learned as much, anyway thanks for your help in advance.
I was thinking of adding another field to each note, something that won't change so i can easily identify and edit each note.
Yes, you absolutely need an immutable identifier for each note document in the firestore so you can unambiguously reference it. You almost always want this whenever you're storing a data object, in any application with any database.
But, the firestore already does this for you: after calling db.collection(user.uid).doc() you should get a doc with an ID. That's the ID you want to use when updating the note.
The part of your code that interacts with the DOM will need to keep track of this. I suggest moving the code the creates the firestore document into the constructor of CreateNote and storing it on this. You'll need the user id there as well.
constructor(title, body, userId) {
this.title = title;
this.body = body;
const docRef = db.collection(userId).doc();
this.docId = docRef.id;
/* etc. */
Then any time you have an instance of CreateNote, you'll know the right user and document to reference.
Other suggestions (since you asked)
Use JsPrettier. It's worth the setup, you'll never go back.
Use HTML semantics correctly. Divs shouldn't be appended as children of hrs, because they're for "a thematic break between paragraph-level elements: for example, a change of scene in a story, or a shift of topic within a section." MDN
For your next project, use a framework. Essentially no one hand-codes event listeners and appends children to get things done. I see the value for basic understanding, but there's a rich and beautiful world of frameworks out there; don't limit yourself by avoiding them :-)
I'm trying to do what I 'thought' would be a simple task. I have an array of URLs that I'd like to loop through and download on to the client machine when the user clicks a button.
Right now I have a parent component that contains the button and an array of the urls (in the state) that I'd like to loop through and download. For some reason, the way I'm doing it now only downloads one of the files, not all of the contents of the array.
Any idea how to do this correctly within React?
handleDownload(event){
var downloadUrls = this.state.downloadUrls;
downloadUrls.forEach(function (value) {
console.log('yo '+value)
const response = {
file: value,
};
window.location.href = response.file;
})
}
I would use setTimeout to wait a little bit between downloading each files.
handleDownload(event){
var downloadUrls = this.state.downloadUrls.slice();
downloadUrls.forEach(function (value, idx) {
const response = {
file: value,
};
setTimeout(() => {
window.location.href = response.file;
}, idx * 100)
})
}
In Chrome, this will also prompt the permission asking for multiple files download.
Been at this one for a LONG time and can't quite figure it out.
I have two components, controlled by a parent component. There is a property called "selected". So, when a user clicks on a list, it will update the parent component's selected property which is passed to the TagInput, which uses a MentionPlugin from draft-js.
In order to handle this, I implement a componentWillReceiveProps that looks as follows.
componentWillReceiveProps(nextProps) {
const { initialTags: newTags } = nextProps;
const previousTags = this.getTags(this.state.editorState);
if (previousTags.length !== newTags.length) {
const added = newTags.filter(tag => !previousTags.includes(tag));
const removed = previousTags.filter(tag => !newTags.includes(tag));
this.addMentions(added);
this.removeMentions(removed);
}
}
While it's easy to add entities in addMentions by creating a new entity and inserting it, for the life of me, I cannot figure out how to get a Mention by text and then delete it from the editor.
removeMentions(tags) {
const { editorState } = this.state;
for (const tag of tags) {
// find tag in editor
// select it and remove it
}
}
How would this be done?