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 @@
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);