diff --git a/flashcards.css b/flashcards.css index d2ea3b4..54d275d 100644 --- a/flashcards.css +++ b/flashcards.css @@ -97,6 +97,13 @@ button { min-width: 12em; } +#adv-settings label input[type="radio"] { + min-width: unset; + margin: 0 0.25em 0 1.5em; +} + +#adv-settings label input[type="radio"]:first-of-type { margin-left: 0; } + #adv-settings button { margin: 0 0.5em; } @@ -111,12 +118,58 @@ input[name=importFileName] { flex-direction: column; flex-grow: 1; justify-content: space-around; + } -#card-content { margin: auto; } -#card-content.small-text{ font-size: 100%; } -#card-content.medium-text{ font-size: 200%; } -#card-content.large-text{ font-size: 800%; } +#card-input { + flex-grow: 2; + display: flex; + flex-direction: row; +} + +#card-input label { + display: flex; + flex-direction: column; + align-items: stretch; + flex-grow: 1; + margin: 0 0.5em; +} + +.card { + display: none; + flex-direction: column; + align-items: center; + width: 100%; +} + +.card.current { display: flex; } + +.card div { + display: flex; + flex-direction: column; + align-items: center; +} + +.card img { + height: auto; + max-height: 100%; + max-width: 100%; +} + +.answer { opacity: 0; } +.card.show-answer .answer { opacity: 1; } + +.card.small img { width: 50%; } +.card.medium img { width: 75%; } +.card.large img { width: 100%; } + +.card.small .prompt { font-size: 100%; } +.card.medium .prompt { font-size: 200%; } +.card.large .prompt { font-size: 800%; } + +.card.small .answer { font-size: 75%; } +.card.medium .answer { font-size: 150%; } +.card.large .answer { font-size: 400%; } /* Card View Visible */ #settings { @@ -132,6 +185,14 @@ input[name=importFileName] { display: none; } +#card-input label.only-with-answers, +#adv-settings label.only-with-answers +{ display: none; } + +.cards-have-answers #card-input label.only-with-answers, +.cards-have-answers #adv-settings label.only-with-answers +{ display: flex; } + /* Mobile View */ @media ( max-width: 600px ) { body { @@ -162,7 +223,7 @@ input[name=importFileName] { height: calc(100vh - 2em); width: calc(100vw - 2em); } - + #settings > button, #cards > button { font-size: 125%; margin: 1em auto; @@ -194,5 +255,3 @@ input[name=importFileName] { bottom: 0; right: 0; } - - diff --git a/flashcards.html b/flashcards.html index f2a62f1..05407ce 100644 --- a/flashcards.html +++ b/flashcards.html @@ -13,15 +13,30 @@

Simple Flashcards

- - +
+ + +
+ + +
-
diff --git a/flashcards.js b/flashcards.js index 3082132..7c94530 100644 --- a/flashcards.js +++ b/flashcards.js @@ -1,14 +1,18 @@ (function() { const FC = { currentSet: [], - items: [], + cards: [], + curCardEls: [], // array of card HTMLElements nextCardIdx: 0, - cardOrder: [], // elements are objects: { name: 'abc', cards: 'xyz' } - savedSets: [], + cardOrder: [], + savedSets: [], // elements are objects: { name: 'abc', cards: 'xyz' } $: document.querySelector.bind(document), version: "%VERSION%" }; + const IMG_REGEX = /(http|https|file):\/\/(.*)\.(jpg|jpeg|gif|png|svg)/; + const URL_REGEX = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/; + FC.shuffle = function(inArray) { /* http://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array * modified to be a pure function, not mutating it's input */ @@ -35,6 +39,10 @@ return outArray; }; + FC.zipItems = function(prompts, answers) { + return prompts.map((prompt, idx) => ({ prompt, answer: answers[idx] || null })); + } + FC.isRunning = function() { return !FC.bodyEl.classList.contains('settings-visible'); }; @@ -47,11 +55,12 @@ if (ev && ev.preventDefault) ev.preventDefault(); const newSet = { - name: (FC.currentSet || {}).name || - FC.$('input[name=saveSetName]').value || - 'new set', - cards: FC.itemsEl.value, - cardPeriod: parseInt(FC.$('input[name=cardPeriod]').value) || 3, + name: FC.$('input[name=saveSetName]').value || 'new set', + prompts: FC.promptsInputEl.value, + answers: FC.answersInputEl.value, + cardsHaveAnswers: FC.$('input[name=hasAnswers]:checked')?.value === 'yes', + promptPeriod: parseInt(FC.$('input[name=promptPeriod]').value) || 3, + answerPeriod: parseInt(FC.$('input[name=answerPeriod]').value) || 3, textSize: FC.$('select[name=textSize]').value || 'small', sortOrder: FC.$('select[name=cardOrder]').value || 'in-order' }; @@ -61,10 +70,15 @@ FC.populateSettingsFromSet = function(set) { FC.$('input[name=saveSetName]').value = set.name; - FC.itemsEl.value = set.cards; - FC.$('input[name=cardPeriod]').value = set.cardPeriod; + FC.promptsInputEl.value = set.prompts; + FC.answersInputEl.value = set.answers; + FC.$('input[name=promptPeriod]').value = set.promptPeriod; + FC.$('input[name=answerPeriod]').value = set.answerPeriod; FC.$('select[name=cardOrder]').value = set.sortOrder; FC.$('select[name=textSize]').value = set.textSize; + FC.$('input[name=hasAnswers][value="' + + (set.cardsHaveAnswers?'yes':'no') + '"]').checked = true; + FC.setCardsHaveAnswers(); }; FC.startCards = function(ev) { @@ -73,15 +87,18 @@ ev.preventDefault(); FC.currentSet = FC.makeSetFromSettings(); - FC.items = FC.currentSet.cards.split('\n'); + FC.cards = FC.zipItems( + FC.currentSet.prompts.split('\n'), + FC.currentSet.answers?.split('\n') + ); - FC.cardContentEl.classList.remove( - 'small-text', 'medium-text', 'large-text'); - FC.cardContentEl.classList.add(FC.currentSet.textSize + '-text'); + FC.curCardEls = FC.cards.map(FC.makeCard); + + FC.curCardEls.forEach(cardEl => FC.cardsEl.prepend(cardEl)); FC.nextCardIdx = 0; - const orderedIndices = [...Array(FC.items.length).keys()]; + const orderedIndices = [...Array(FC.cards.length).keys()]; switch (FC.currentSet.sortOrder) { default: case 'in-order': @@ -96,20 +113,60 @@ FC.$('html').requestFullscreen(); FC.showNextCard(); - FC.runningInterval = setInterval(FC.showNextCard, FC.currentSet.cardPeriod * 1000); + const fullInterval = (FC.currentSet.promptPeriod * 1000) + + (FC.currentSet.cardsHaveAnswers ? FC.currentSet.answerPeriod * 1000 : 0); + FC.runningInterval = setInterval(FC.showNextCard, fullInterval); FC.bodyEl.classList.remove('settings-visible'); }; + FC.makeCard = function(item, idx) { + const newCardDiv = document.createElement("div"); + newCardDiv.classList.add('card', FC.currentSet.textSize); + newCardDiv.dataset.index = idx; + + const promptDiv = document.createElement("div"); + promptDiv.classList.add('prompt'); + promptDiv.innerHTML = FC.transformContent(item.prompt); + + const answerDiv = document.createElement("div"); + answerDiv.classList.add('answer'); + answerDiv.innerHTML = FC.transformContent(item.answer); + + newCardDiv.appendChild(promptDiv); + newCardDiv.appendChild(answerDiv); + return newCardDiv; + }; + + FC.transformContent = function(content) { + const matchesImg = content.toLowerCase().trim().match(IMG_REGEX); + if (matchesImg) { + return ''; + } else { + return content; + } + }; + FC.stopCards = function(ev) { clearInterval(FC.runningInterval); document.exitFullscreen(); FC.bodyEl.classList.add('settings-visible'); + document.querySelectorAll('.card').forEach(el => el.remove()); }; FC.showNextCard = function() { - const curItem = FC.items[FC.cardOrder[FC.nextCardIdx]]; - FC.cardContentEl.innerHTML = curItem; - FC.nextCardIdx = (FC.nextCardIdx + 1) % FC.items.length; + FC.$('.card.current')?.classList?.remove('current', 'show-answer'); + + const curCardEl = FC.$('.card[data-index="' + FC.cardOrder[FC.nextCardIdx] + '"]'); + curCardEl.classList.add('current'); + + FC.nextCardIdx = (FC.nextCardIdx + 1) % FC.cards.length; + + if (FC.currentSet.cardsHaveAnswers) { + setTimeout( + () => { curCardEl.classList.add('show-answer') }, + FC.currentSet.promptPeriod * 1000 + ); + } }; FC.handleLoadSet = function(ev) { @@ -214,13 +271,37 @@ reader.readAsText(fileToImport); }; + FC.importCardSetFromURL = function(ev) { + fetch(FC.$('input[name=importURL]').value) + .then(resp => resp.json()) + .then(set => { + if (!set.name) throw "Invalid set definition file (missing name)"; + FC.saveSet(set); + FC.storeSets(FC.savedSets); + FC.updateSetList(FC.savedSets); + FC.populateSettingsFromSet(set); + }) + .catch(err => { + alert("Unable to load set from URL: " + err); + console.error(err); + }); + }; + + FC.setCardsHaveAnswers = function(ev) { + if (FC.$('input[name=hasAnswers]:checked')?.value === 'yes') { + FC.bodyEl.classList.add('cards-have-answers'); + } else { + FC.bodyEl.classList.remove('cards-have-answers'); + } + }; + window.onload = function() { // Cached element references FC.settingsEl = FC.$('#settings'); - FC.itemsEl = FC.$('#items-input'); + FC.promptsInputEl = FC.$('#prompts-input'); + FC.answersInputEl = FC.$('#answers-input'); FC.bodyEl = FC.$('body'); FC.cardsEl = FC.$('#cards'); - FC.cardContentEl = FC.$('#card-content'); // Event handlers FC.$('.toggle-adv-settings').addEventListener('click', FC.toggleAdvSettings); @@ -228,8 +309,11 @@ FC.$('#stop-button').addEventListener('click', FC.stopCards); FC.$('#export').addEventListener('click', FC.exportCardSet); FC.$('#import').addEventListener('click', FC.importCardSet); + FC.$('#importURL').addEventListener('click', FC.importCardSetFromURL); FC.$('input[name=importFileName]') .addEventListener('change', FC.handleImport); + document.querySelectorAll('input[name=hasAnswers]').forEach(inpEl => + inpEl.addEventListener('change', FC.setCardsHaveAnswers)); FC.$('#save-set').addEventListener('click', FC.handleSaveSet); FC.$('#load-set').addEventListener('click', FC.handleLoadSet);