Compare commits

..

No commits in common. "ab75db9a392893aa4c052ee7e8df71380b873160" and "345b606571d976e557e29e0dd0a68558a5e00c6c" have entirely different histories.

34 changed files with 231 additions and 626 deletions

View File

@ -25,3 +25,9 @@ It's less useful, but `npm run serve-frontend-only` will bundle and serve _only_
## Production
`npm run bundle` bundles and minifies the frontend stuff and also copies the backend stuff to `dist`. Be sure to run `npm run clear` to delete the contents of `dist` and `.cache` before using `npm run bundle` to make sure you don't get old dev versions of the bundled code included in your upload.
## UpUp Configuration
[UpUp](https://github.com/TalAter/UpUp) is a tool that enables browsers to download an offline version of a website so users can still access it if they lose internet connection. Because Parcel Bundler hashes every file accessed via reference within the code, you need to ensure that the UpUp configuration at the bottom of `index.html` is kept up to date whenever you make changes to files. Typically the only file hashes that will change are `src.*.js` and `main.*.css`, but it's best to check all of them just to make sure.
After bundling, update the files referenced in the configuration to make sure UpUp can download the files correctly, then bundle again so `dist/index.html` gets updated. I'm desperately hoping I can find a way to automate this in the build process, but I haven't figured it out just yet. Maybe I'll end up using `router.php` and `.htaccess` to do the heavy lifting for me. We'll see.

View File

@ -121,7 +121,6 @@
</aside>
<section id="mainColumn">
{{announcements}}
<section id="detailsSection">
<h2 id="dictionaryName">Dictionary Name</h2>
<nav>
@ -370,6 +369,33 @@
<div id="messagingSection"></div>
{{upup_insert}}
<script src="//cdnjs.cloudflare.com/ajax/libs/UpUp/1.1.0/upup.min.js"></script>
<script>
window.onload = (function (oldLoad) {
return function () {
oldLoad && oldLoad();
if (UpUp) {
UpUp.start({
'cache-version': '2.0.0',
'content-url': 'offline.html',
'assets': [
'src.8d47de22.js',
'main.3381ca43.css',
'help.ecc721dc.html',
'privacy.5cbf7ea0.html',
'terms.bcb4cecc.html',
'usage.4a51122f.html',
'ads.f4e0c99c.js',
'favicon.5d013634.png',
'ipa-table.6c5cf939.html',
'papaparse.min.dab93fe4.js',
],
'service-worker-url': 'upup.sw.min.js',
});
}
}
})(window.onload);
</script>
</body>
</html>

View File

@ -2,19 +2,17 @@
"name": "lexiconga-lite",
"version": "1.0.0",
"description": "A light-as-possible rewrite of Lexiconga",
"main": "template-index.html",
"main": "index.html",
"repository": "https://cybre.tech/Alamantus/lexiconga-lite.git",
"author": "Robbie Antenesse <dev@alamantus.com>",
"license": "UNLICENCED",
"scripts": {
"start": "concurrently \"npm run watch-js\" \"npm run watch-php\" \"npm run copy-files\"",
"watch-js": "parcel watch template-index.html offline.html template-view.html template-passwordreset.html --no-hmr --public-url ./",
"watch-js": "parcel watch index.html offline.html template-view.html template-passwordreset.html --no-hmr --public-url ./",
"watch-php": "cpx \"src/php/**/{*,.*}\" dist -v -w",
"bundle": "npm run bundle-js && npm run copy-files && npm run copy-php",
"bundle-js": "parcel build template-index.html offline.html template-view.html template-passwordreset.html",
"copy-files": "cpx \"node_modules/upup/dist/*.min.js\" dist -v",
"copy-php": "cpx \"src/php/**/{*,.*}\" dist",
"serve-frontend-only": "parcel template-index.html",
"copy-files": "cpx \"node_modules/upup/dist/upup.sw.min.js\" dist -v",
"bundle": "parcel build index.html offline.html template-view.html template-passwordreset.html && npm run copy-files && cpx \"src/php/**/{*,.*}\" dist",
"serve-frontend-only": "parcel index.html",
"clear": "npm run clear-dist && npm run clear-cache",
"clear-dist": "rimraf dist/*",
"clear-cache": "rimraf .cache/*"

View File

@ -20,11 +20,12 @@ function initialize() {
});
}
setupAds();
renderAll();
setupAds().then(() => renderAll());
}
window.onload = (function (oldLoad) {
oldLoad && oldLoad();
initialize();
return function () {
oldLoad && oldLoad();
initialize();
}
})(window.onload);

View File

@ -1,3 +1,4 @@
import { setupInfoModal } from "../setupListeners";
import { request } from "./helpers";
export function renderForgotPasswordForm() {
@ -23,15 +24,6 @@ export function renderForgotPasswordForm() {
setupInfoModal(modal);
}
function setupInfoModal(modal) {
const closeElements = modal.querySelectorAll('.modal-background, .close-button');
Array.from(closeElements).forEach(close => {
close.addEventListener('click', () => {
modal.parentElement.removeChild(modal);
});
});
}
function setupStartResetForm() {
document.getElementById('forgotPasswordSubmit').addEventListener('click', startPasswordReset);
}
@ -67,10 +59,7 @@ function startPasswordReset() {
}
function setupPasswordResetForm() {
const submitButton = document.getElementById('newPasswordSubmit');
if (submitButton) {
submitButton.addEventListener('click', submitPasswordReset);
}
document.getElementById('newPasswordSubmit').addEventListener('click', submitPasswordReset);
}
function submitPasswordReset() {
@ -108,6 +97,8 @@ function submitPasswordReset() {
}
window.onload = (function (oldLoad) {
oldLoad && oldLoad();
setupPasswordResetForm();
return function () {
oldLoad && oldLoad();
setupPasswordResetForm();
}
})(window.onload);

View File

@ -1,14 +1,41 @@
import { addMessage } from "../utilities";
import { saveDictionary, clearDictionary } from "../dictionaryManagement";
import { request } from "./helpers";
import { saveToken, dictionaryIsDefault } from "./utilities";
import { saveToken } from "./utilities";
import { renderAll } from "../render";
import { sortWords } from "../wordManagement";
import { getLocalDeletedWords, clearLocalDeletedWords, saveDeletedWordsLocally } from "./utilities";
import { renderChangeDictionaryOptions } from "./render";
/* Outline for syncing
login
-> check local dictionary id
(DONE!) ? no id
-> upload dictionary
-> make new dictionary current
(Canceled) ? mismatched id
-> sync local dictionary (see 'same id' below)
-> if no matching remote id, ignore (assume deleted)
-> clear local dictionary
-> insert downloaded dictionary
(DONE!) ? same id
-> compare detail last updated timestamp
? downloaded details are newer
-> replace local details
? local details are newer
-> flag to upload details
-> filter deleted words from current words
-- check id and compare deletedOn with createdOn
-> compare each word and by lastUpdated/createdOn
? downloaded word is newer
-> update local word
? local word is newer
-> put word in an array to upload
-> upload anything that needs update
*/
export function syncDictionary(uploadAsNewIfNoExternalID = true) {
if (!window.currentDictionary.hasOwnProperty('externalID') && !dictionaryIsDefault()) {
if (!window.currentDictionary.hasOwnProperty('externalID')) {
uploadWholeDictionary(uploadAsNewIfNoExternalID);
} else {
addMessage('Syncing...');

View File

@ -1,25 +1,11 @@
import { setCookie } from "../StackOverflow/cookie";
import { DELETED_WORDS_LOCALSTORAGE_KEY } from "./constants";
import { getTimestampInSeconds, cloneObject } from "../../helpers";
import { DEFAULT_DICTIONARY } from "../../constants";
import { getTimestampInSeconds } from "../../helpers";
export function saveToken(token) {
setCookie('token', token, 30);
}
export function dictionaryIsDefault() {
const defaultDictionary = cloneObject(DEFAULT_DICTIONARY);
delete defaultDictionary.lastUpdated;
delete defaultDictionary.createdOn;
delete defaultDictionary.version;
const currentDictionary = cloneObject(window.currentDictionary);
delete currentDictionary.lastUpdated;
delete currentDictionary.createdOn;
delete currentDictionary.version;
console.log(JSON.stringify(defaultDictionary) === JSON.stringify(currentDictionary));
return JSON.stringify(defaultDictionary) === JSON.stringify(currentDictionary);
}
export function saveDeletedWordsLocally(wordIds) {
let storedDeletedWords = getLocalDeletedWords();
wordIds.forEach(wordId => {

View File

@ -1,11 +1,12 @@
import { DISPLAY_AD_EVERY } from '../constants.js';
import ads from '../../ads.json';
export function setupAds() {
const shuffle = (a, b) => Math.random() > 0.5 ? 1 : -1;
const priority = ads.filter(ad => isActive(ad) && ad.isPriority).sort(shuffle);
const regular = ads.filter(ad => isActive(ad) && !ad.isPriority).sort(shuffle);
window.ads = [...priority, ...regular];
return import('../../ads.json').then(ads => {
const shuffle = (a, b) => Math.random() > 0.5 ? 1 : -1;
const priority = ads.filter(ad => isActive(ad) && ad.isPriority).sort(shuffle);
const regular = ads.filter(ad => isActive(ad) && !ad.isPriority).sort(shuffle);
window.ads = [...priority, ...regular];
});
}
function isActive(ad) {

View File

@ -1,4 +1,3 @@
import papa from 'papaparse';
import { renderDictionaryDetails, renderPartsOfSpeech, renderAll, renderTheme } from "./render";
import { removeTags, cloneObject, getTimestampInSeconds, download, slugify } from "../helpers";
import { LOCAL_STORAGE_KEY, DEFAULT_DICTIONARY, MIGRATE_VERSION } from "../constants";
@ -204,48 +203,50 @@ export function importWords() {
if (importWordsField.files.length === 1) {
if (confirm('Importing a CSV file with words will add all of the words in the file to your dictionary regardless of duplication!\nDo you want to continue?')) {
addMessage('Importing words...');
const importedWords = [];
papa.parse(importWordsField.files[0], {
header: true,
encoding: "utf-8",
step: results => {
if (results.errors.length > 0) {
results.errors.forEach(err => {
addMessage('Error Importing Word: ' + err, undefined, 'error');
console.error('Error Importing Word: ', err)
});
} else {
const row = results.data[0];
const importedWord = addWord({
name: removeTags(row.word).trim(),
pronunciation: removeTags(row.pronunciation).trim(),
partOfSpeech: removeTags(row['part of speech']).trim(),
definition: removeTags(row.definition).trim(),
details: removeTags(row.explanation).trim(),
wordId: getNextId(),
}, false, false, false);
import('papaparse').then(papa => {
const importedWords = [];
papa.parse(importWordsField.files[0], {
header: true,
encoding: "utf-8",
step: results => {
if (results.errors.length > 0) {
results.errors.forEach(err => {
addMessage('Error Importing Word: ' + err, undefined, 'error');
console.error('Error Importing Word: ', err)
});
} else {
const row = results.data[0];
const importedWord = addWord({
name: removeTags(row.word).trim(),
pronunciation: removeTags(row.pronunciation).trim(),
partOfSpeech: removeTags(row['part of speech']).trim(),
definition: removeTags(row.definition).trim(),
details: removeTags(row.explanation).trim(),
wordId: getNextId(),
}, false, false, false);
importedWords.push(importedWord);
}
},
complete: () => {
saveDictionary(false);
renderAll();
importWordsField.value = '';
document.getElementById('editModal').style.display = 'none';
addMessage(`Done Importing ${importedWords.length} Words`);
importedWords.push(importedWord);
}
},
complete: () => {
saveDictionary(false);
renderAll();
importWordsField.value = '';
document.getElementById('editModal').style.display = 'none';
addMessage(`Done Importing ${importedWords.length} Words`);
if (hasToken()) {
import('./account/index.js').then(account => {
account.syncImportedWords(importedWords);
});
}
},
error: err => {
addMessage('Error Importing Words: ' + err, undefined, 'error');
console.error('Error Importing Words: ', err);
},
skipEmptyLines: true,
if (hasToken()) {
import('./account/index.js').then(account => {
account.syncImportedWords(importedWords);
});
}
},
error: err => {
addMessage('Error Importing Words: ' + err, undefined, 'error');
console.error('Error Importing Words: ', err);
},
skipEmptyLines: true,
});
});
}
}
@ -268,21 +269,23 @@ export function exportWords() {
addMessage('Exporting Words...');
setTimeout(() => {
const { name, specification } = window.currentDictionary;
const fileName = slugify(name + '_' + specification) + '_words.csv';
const words = window.currentDictionary.words.map(word => {
return {
word: word.name,
pronunciation: word.pronunciation,
'part of speech': word.partOfSpeech,
definition: word.definition,
explanation: word.details,
}
import('papaparse').then(papa => {
const { name, specification } = window.currentDictionary;
const fileName = slugify(name + '_' + specification) + '_words.csv';
const words = window.currentDictionary.words.map(word => {
return {
word: word.name,
pronunciation: word.pronunciation,
'part of speech': word.partOfSpeech,
definition: word.definition,
explanation: word.details,
}
});
const csv = papa.unparse(words, { quotes: true });
download(csv, fileName, 'text/csv;charset=utf-8');
});
const csv = papa.unparse(words, { quotes: true });
download(csv, fileName, 'text/csv;charset=utf-8');
}, 1);
}

View File

@ -5,7 +5,6 @@ import { showSearchModal, clearSearchText } from "./search";
import { saveAndCloseSettingsModal, openSettingsModal, saveSettings } from "./settings";
import { saveAndCloseEditModal, openEditModal } from "./dictionaryManagement";
import { addMessage, hideAllModals } from "./utilities";
import helpFile from '../markdown/help.md';
export function enableHotKeys() {
document.addEventListener('keydown', hotKeyActions);
@ -103,7 +102,9 @@ function submitWord() {
}
function showHelpModal() {
renderInfoModal(helpFile);
import('../markdown/help.md').then(html => {
renderInfoModal(html);
});
}
function maximizeTextarea() {

View File

@ -17,7 +17,6 @@ import {
import { getPaginationData } from './pagination';
import { getOpenEditForms, parseReferences } from './wordManagement';
import { renderAd } from './ads';
import ipaTableFile from './KeyboardFire/phondue/ipa-table.html';
export function renderAll() {
renderTheme();
@ -268,16 +267,18 @@ export function renderEditForm(wordId = false) {
wordId = typeof wordId.target === 'undefined' ? wordId : parseInt(this.id.replace('edit_', ''));
const word = window.currentDictionary.words.find(w => w.wordId === wordId);
if (word) {
const ipaPronunciationField = `<input id="wordPronunciation_${wordId}" class="ipa-field" maxlength="200" value="${word.pronunciation}"><br>
<a class="label-help-button ipa-field-help-button">Field Help</a>`;
const plainPronunciationField = `<input id="wordPronunciation_${wordId}" maxlength="200" value="${word.pronunciation}">`;
const ipaPronunciationField = `<label>Pronunciation<a class="label-button ipa-table-button">IPA Chart</a><br>
<input id="wordPronunciation_${wordId}" class="ipa-field" maxlength="200" value="${word.pronunciation}"><br>
<a class="label-help-button ipa-field-help-button">Field Help</a>
</label>`;
const plainPronunciationField = `<label>Pronunciation<br>
<input id="wordPronunciation_${wordId}" maxlength="200" value="${word.pronunciation}">
</label>`;
const editForm = `<form id="editForm_${wordId}" class="edit-form">
<label>Word<span class="red">*</span><br>
<input id="wordName_${wordId}" maxlength="200" value="${word.name}">
</label>
<label>Pronunciation<a class="label-button ipa-table-button">IPA Chart</a><br>
${window.settings.useIPAPronunciationField ? ipaPronunciationField : plainPronunciationField}
</label>
${window.settings.useIPAPronunciationField ? ipaPronunciationField : plainPronunciationField}
<label>Part of Speech<br>
<select id="wordPartOfSpeech_${wordId}" class="part-of-speech-select">
<option value="${word.partOfSpeech}" selected>${word.partOfSpeech}</option>
@ -310,22 +311,24 @@ export function renderIPATable(ipaTableButton) {
ipaTableButton = typeof ipaTableButton.target === 'undefined' || ipaTableButton.target === '' ? ipaTableButton : ipaTableButton.target;
const label = ipaTableButton.parentElement.innerText.replace(/(Field Help|IPA Chart)/g, '').trim();
const textBox = ipaTableButton.parentElement.querySelector('input');
const modalElement = document.createElement('section');
modalElement.classList.add('modal', 'ipa-table-modal');
modalElement.innerHTML = `<div class="modal-background"></div>
<div class="modal-content">
<a class="close-button">&times;&#xFE0E;</a>
<header><label>${label} <input value="${textBox.value}" class="ipa-field"></label></header>
<section>
${ipaTableFile}
</section>
<footer><a class="button done-button">Done</a></footer>
</div>`;
document.body.appendChild(modalElement);
import('./KeyboardFire/phondue/ipa-table.html').then(html => {
const modalElement = document.createElement('section');
modalElement.classList.add('modal', 'ipa-table-modal');
modalElement.innerHTML = `<div class="modal-background"></div>
<div class="modal-content">
<a class="close-button">&times;&#xFE0E;</a>
<header><label>${label} <input value="${textBox.value}" class="ipa-field"></label></header>
<section>
${html}
</section>
<footer><a class="button done-button">Done</a></footer>
</div>`;
setupIPAFields();
setupIPATable(modalElement, textBox);
document.body.appendChild(modalElement);
setupIPAFields();
setupIPATable(modalElement, textBox);
});
}
export function renderMaximizedTextbox(maximizeButton) {

View File

@ -68,7 +68,7 @@ export function toggleHotkeysEnabled() {
}
export function toggleIPAPronunciationFields(render = true) {
const ipaButtons = document.querySelectorAll('.ipa-field-help-button'),
const ipaButtons = document.querySelectorAll('.ipa-table-button, .ipa-field-help-button'),
ipaFields = document.querySelectorAll('.ipa-field');
if (!window.settings.useIPAPronunciationField) {
Array.from(ipaButtons).forEach(button => {

View File

@ -8,9 +8,6 @@ import { usePhondueDigraphs } from './KeyboardFire/phondue/ipaField';
import { openSettingsModal, saveSettingsModal, saveAndCloseSettingsModal } from './settings';
import { enableHotKeys } from './hotkeys';
import { showSearchModal, clearSearchText, checkAllPartsOfSpeechFilters, uncheckAllPartsOfSpeechFilters } from './search';
import helpFile from '../markdown/help.md';
import termsFile from '../markdown/terms.md';
import privacyFile from '../markdown/privacy.md';
export default function setupListeners() {
setupDetailsTabs();
@ -360,13 +357,19 @@ export function setupMaximizeModal(modal, textBox) {
export function setupInfoButtons() {
document.getElementById('helpInfoButton').addEventListener('click', () => {
renderInfoModal(helpFile);
import('../markdown/help.md').then(html => {
renderInfoModal(html);
});
});
document.getElementById('termsInfoButton').addEventListener('click', () => {
renderInfoModal(termsFile);
import('../markdown/terms.md').then(html => {
renderInfoModal(html);
});
});
document.getElementById('privacyInfoButton').addEventListener('click', () => {
renderInfoModal(privacyFile);
import('../markdown/privacy.md').then(html => {
renderInfoModal(html);
});
});
}

View File

@ -0,0 +1,12 @@
export function getDictionary() {
const url = window.location.href.replace(/\#.*$/gi, '');
console.log(url);
let dict = url.substr(url.lastIndexOf('?'));
console.log(dict);
if (dict === url) {
dict = dict.substr(dict.lastIndexOf('/'));
console.log(dict);
}
dict = dict.replace(/[\?\/]/g, '');
console.log(dict);
}

View File

@ -2,13 +2,20 @@ import { renderAll } from './render';
import setupListeners from './setupListeners';
import { setupAds } from '../ads';
// import setupListeners, { setupSearchFilters } from './js/setupListeners';
// import { renderAll } from './js/render';
// import { hasToken } from './js/utilities';
// import { loadDictionary } from './js/dictionaryManagement';
// import { loadSettings } from './js/settings';
function initialize() {
setupAds();
renderAll();
setupAds().then(() => renderAll());
setupListeners();
}
window.onload = (function (oldLoad) {
oldLoad && oldLoad();
initialize();
return function () {
oldLoad && oldLoad();
initialize();
}
})(window.onload);

View File

@ -1,26 +1,20 @@
import md from 'marked';
import { removeTags, slugify } from '../../helpers';
import { getWordsStats, getHomonymnNumber } from './utilities';
import { getMatchingSearchWords, highlightSearchTerm, getSearchFilters, getSearchTerm } from './search';
import { showSection } from './displayToggles';
import { getWordsStats, wordExists } from '../utilities';
import { getMatchingSearchWords, highlightSearchTerm, getSearchFilters, getSearchTerm } from '../search';
import { showSection } from '../displayToggles';
import { setupSearchFilters, setupInfoModal } from './setupListeners';
import { parseReferences } from './wordManagement';
import { parseReferences } from '../wordManagement';
import { renderTheme } from '../render';
import { renderAd } from '../ads';
import { sortWords } from './wordManagement';
export function renderAll() {
renderTheme();
renderDictionaryDetails();
renderPartsOfSpeech();
sortWords();
renderWords();
}
export function renderTheme() {
const { theme } = window.currentDictionary.settings;
document.body.id = theme + 'Theme';
}
export function renderDictionaryDetails() {
renderName();
@ -155,15 +149,13 @@ export function renderWords() {
details: parseReferences(removeTags(originalWord.details)),
wordId: originalWord.wordId,
});
const homonymnNumber = getHomonymnNumber(originalWord);
const shareLink = window.location.pathname + (window.location.pathname.match(new RegExp(word.wordId + '$')) ? '' : '/' + word.wordId);
wordsHTML += renderAd(displayIndex);
wordsHTML += `<article class="entry" id="${word.wordId}">
<header>
<h4 class="word">${word.name}${homonymnNumber > 0 ? ' <sub>' + homonymnNumber.toString() + '</sub>' : ''}</h4>
<h4 class="word">${word.name}</h4>
<span class="pronunciation">${word.pronunciation}</span>
<span class="part-of-speech">${word.partOfSpeech}</span>
<a href="${shareLink}" target="_blank" class="small button word-option-button" title="Link to Word">&#10150;</a>

View File

@ -1,145 +0,0 @@
import { cloneObject, getIndicesOf } from "../../helpers";
import removeDiacritics from "../StackOverflow/removeDiacritics";
import { renderWords } from "./render";
export function showSearchModal() {
document.getElementById('searchModal').style.display = 'block';
document.getElementById('searchBox').focus();
}
export function clearSearchText() {
document.getElementById('searchBox').value = '';
document.getElementById('openSearchModal').value = '';
renderWords();
}
export function getSearchTerm() {
return document.getElementById('searchBox').value;
}
export function getSearchFilters() {
const filters = {
caseSensitive: document.getElementById('searchCaseSensitive').checked,
ignoreDiacritics: document.getElementById('searchIgnoreDiacritics').checked,
exact: document.getElementById('searchExactWords').checked,
name: document.getElementById('searchIncludeName').checked,
definition: document.getElementById('searchIncludeDefinition').checked,
details: document.getElementById('searchIncludeDetails').checked,
partsOfSpeech: {},
};
const partsOfSpeech = document.querySelectorAll('#searchPartsOfSpeech input[type="checkbox"]');
let checkedBoxes = 0;
Array.from(partsOfSpeech).forEach(partOfSpeech => {
// console.log('partOfSpeech Inner Text:', partOfSpeech.parentElement.innerText);
const partOfSpeechLabel = partOfSpeech.parentElement.innerText.trim();
filters.partsOfSpeech[partOfSpeechLabel] = partOfSpeech.checked;
if (partOfSpeech.checked) checkedBoxes++;
});
filters.allPartsOfSpeechChecked = checkedBoxes === partsOfSpeech.length;
return filters;
}
export function getMatchingSearchWords() {
let searchTerm = getSearchTerm();
const filters = getSearchFilters();
if (searchTerm !== '' || !filters.allPartsOfSpeechChecked) {
const matchingWords = window.currentDictionary.words.slice().filter(word => {
if (!filters.allPartsOfSpeechChecked) {
const partOfSpeech = word.partOfSpeech === '' ? 'Unclassified' : word.partOfSpeech;
return filters.partsOfSpeech.hasOwnProperty(partOfSpeech) && filters.partsOfSpeech[partOfSpeech];
}
return true;
}).filter(word => {
searchTerm = filters.ignoreDiacritics ? removeDiacritics(searchTerm) : searchTerm;
searchTerm = filters.caseSensitive ? searchTerm : searchTerm.toLowerCase();
let name = filters.ignoreDiacritics ? removeDiacritics(word.name) : word.name;
name = filters.caseSensitive ? name : name.toLowerCase();
let definition = filters.ignoreDiacritics ? removeDiacritics(word.definition) : word.definition;
definition = filters.caseSensitive ? definition : definition.toLowerCase();
let details = filters.ignoreDiacritics ? removeDiacritics(word.details) : word.details;
details = filters.caseSensitive ? details : details.toLowerCase();
const isInName = filters.name && (filters.exact
? searchTerm == name
: new RegExp(searchTerm, 'g').test(name));
const isInDefinition = filters.definition && (filters.exact
? searchTerm == definition
: new RegExp(searchTerm, 'g').test(definition));
const isInDetails = filters.details && new RegExp(searchTerm, 'g').test(details);
return searchTerm === '' || isInName || isInDefinition || isInDetails;
});
return matchingWords;
}
return window.currentDictionary.words
}
export function highlightSearchTerm(word) {
let searchTerm = getSearchTerm();
if (searchTerm) {
const filters = getSearchFilters();
const markedUpWord = cloneObject(word);
if (filters.ignoreDiacritics) {
const searchTermLength = searchTerm.length;
searchTerm = removeDiacritics(searchTerm);
if (filters.name) {
const nameMatches = getIndicesOf(searchTerm, removeDiacritics(markedUpWord.name), filters.caseSensitive);
nameMatches.forEach((wordIndex, i) => {
wordIndex += '<mark></mark>'.length * i;
markedUpWord.name = markedUpWord.name.substring(0, wordIndex)
+ '<mark>' + markedUpWord.name.substr(wordIndex, searchTermLength) + '</mark>'
+ markedUpWord.name.substr(wordIndex + searchTermLength);
});
}
if (filters.definition) {
const definitionMatches = getIndicesOf(searchTerm, removeDiacritics(markedUpWord.definition), filters.caseSensitive);
definitionMatches.forEach((wordIndex, i) => {
wordIndex += '<mark></mark>'.length * i;
markedUpWord.definition = markedUpWord.definition.substring(0, wordIndex)
+ '<mark>' + markedUpWord.definition.substr(wordIndex, searchTermLength) + '</mark>'
+ markedUpWord.definition.substr(wordIndex + searchTermLength);
});
}
if (filters.details) {
const detailsMatches = getIndicesOf(searchTerm, removeDiacritics(markedUpWord.details), filters.caseSensitive);
detailsMatches.forEach((wordIndex, i) => {
wordIndex += '<mark></mark>'.length * i;
markedUpWord.details = markedUpWord.details.substring(0, wordIndex)
+ '<mark>' + markedUpWord.details.substr(wordIndex, searchTermLength) + '</mark>'
+ markedUpWord.details.substr(wordIndex + searchTermLength);
});
}
} else {
const regexMethod = 'g' + (filters.caseSensitive ? '' : 'i');
if (filters.name) {
markedUpWord.name = markedUpWord.name.replace(new RegExp(`(${searchTerm})`, regexMethod), `<mark>$1</mark>`);
}
if (filters.definition) {
markedUpWord.definition = markedUpWord.definition.replace(new RegExp(`(${searchTerm})`, regexMethod), `<mark>$1</mark>`);
}
if (filters.details) {
markedUpWord.details = markedUpWord.details.replace(new RegExp(`(${searchTerm})`, regexMethod), `<mark>$1</mark>`);
}
}
return markedUpWord;
}
return word;
}
export function checkAllPartsOfSpeechFilters() {
const searchFilters = document.querySelectorAll('#searchPartsOfSpeech input[type="checkbox"]');
Array.from(searchFilters).forEach(filter => {
filter.checked = true;
});
renderWords();
}
export function uncheckAllPartsOfSpeechFilters() {
const searchFilters = document.querySelectorAll('#searchPartsOfSpeech input[type="checkbox"]');
Array.from(searchFilters).forEach(filter => {
filter.checked = false;
});
renderWords();
}

View File

@ -1,9 +1,6 @@
import {showSection, hideDetailsPanel} from './displayToggles';
import { showSearchModal, clearSearchText, checkAllPartsOfSpeechFilters, uncheckAllPartsOfSpeechFilters } from './search';
import { showSearchModal, clearSearchText, checkAllPartsOfSpeechFilters, uncheckAllPartsOfSpeechFilters } from '../search';
import { renderWords, renderInfoModal } from './render';
import helpFile from '../../markdown/help.md';
import termsFile from '../../markdown/terms.md';
import privacyFile from '../../markdown/privacy.md';
export default function setupListeners() {
setupDetailsTabs();
@ -82,13 +79,19 @@ export function setupSearchFilters() {
export function setupInfoButtons() {
document.getElementById('helpInfoButton').addEventListener('click', () => {
renderInfoModal(helpFile);
import('../../markdown/help.md').then(html => {
renderInfoModal(html);
});
});
document.getElementById('termsInfoButton').addEventListener('click', () => {
renderInfoModal(termsFile);
import('../../markdown/terms.md').then(html => {
renderInfoModal(html);
});
});
document.getElementById('privacyInfoButton').addEventListener('click', () => {
renderInfoModal(privacyFile);
import('../../markdown/privacy.md').then(html => {
renderInfoModal(html);
});
});
}

View File

@ -1,118 +0,0 @@
export function getWordsStats() {
const {words, partsOfSpeech} = window.currentDictionary;
const {caseSensitive} = window.currentDictionary.settings;
const wordStats = {
numberOfWords: [
{
name: 'Total',
value: words.length,
},
],
wordLength: {
shortest: 0,
longest: 0,
average: 0,
},
letterDistribution: [
/* {
letter: '',
number: 0,
percentage: 0.00,
} */
],
totalLetters: 0,
};
partsOfSpeech.forEach(partOfSpeech => {
const wordsWithPartOfSpeech = words.filter(word => word.partOfSpeech === partOfSpeech);
wordStats.numberOfWords.push({
name: partOfSpeech,
value: wordsWithPartOfSpeech.length,
});
});
wordStats.numberOfWords.push({
name: 'Unclassified',
value: words.filter(word => !partsOfSpeech.includes(word.partOfSpeech)).length,
});
let totalLetters = 0;
const numberOfLetters = {};
words.forEach(word => {
const shortestWord = wordStats.wordLength.shortest;
const longestWord = wordStats.wordLength.longest;
const wordLetters = word.name.split('');
const lettersInWord = wordLetters.length;
totalLetters += lettersInWord;
if (shortestWord === 0 || lettersInWord < shortestWord) {
wordStats.wordLength.shortest = lettersInWord;
}
if (longestWord === 0 || lettersInWord > longestWord) {
wordStats.wordLength.longest = lettersInWord;
}
wordLetters.forEach(letter => {
const letterToUse = caseSensitive ? letter : letter.toLowerCase();
if (!numberOfLetters.hasOwnProperty(letterToUse)) {
numberOfLetters[letterToUse] = 1;
} else {
numberOfLetters[letterToUse]++;
}
});
});
wordStats.totalLetters = totalLetters;
wordStats.wordLength.average = words.length > 0 ? Math.round(totalLetters / words.length) : 0;
for (const letter in numberOfLetters) {
if (numberOfLetters.hasOwnProperty(letter)) {
const number = numberOfLetters[letter];
wordStats.letterDistribution.push({
letter,
number,
percentage: number / totalLetters,
});
}
}
wordStats.letterDistribution.sort((a, b) => {
if (a.percentage === b.percentage) return 0;
return (a.percentage > b.percentage) ? -1 : 1;
});
return wordStats;
}
export function getHomonymnIndexes(word) {
const { currentDictionary } = window;
const { caseSensitive } = currentDictionary.settings;
const foundIndexes = [];
currentDictionary.words.forEach((existingWord, index) => {
if (existingWord.wordId !== word.wordId
&& (caseSensitive ? existingWord.name === word.name : existingWord.name.toLowerCase() === word.name.toLowerCase())) {
foundIndexes.push(index);
}
});
return foundIndexes;
}
export function getHomonymnNumber(word) {
const homonyms = getHomonymnIndexes(word);
if (homonyms.length > 0) {
const index = window.currentDictionary.words.findIndex(w => w.wordId === word.wordId);
let number = 1;
for (let i = 0; i < homonyms.length; i++) {
if (index < homonyms[i]) break;
number++;
}
return number;
}
return 0;
}

View File

@ -1,55 +0,0 @@
import { wordExists, getHomonymnIndexes } from "./utilities";
import removeDiacritics from "../StackOverflow/removeDiacritics";
export function sortWords() {
const { sortByDefinition } = window.currentDictionary.settings;
const sortBy = sortByDefinition ? 'definition' : 'name';
window.currentDictionary.words.sort((wordA, wordB) => {
if (removeDiacritics(wordA[sortBy]).toLowerCase() === removeDiacritics(wordB[sortBy]).toLowerCase()) return 0;
return removeDiacritics(wordA[sortBy]).toLowerCase() > removeDiacritics(wordB[sortBy]).toLowerCase() ? 1 : -1;
});
}
export function parseReferences(detailsMarkdown) {
const references = detailsMarkdown.match(/\{\{.+?\}\}/g);
if (references && Array.isArray(references)) {
new Set(references).forEach(reference => {
let wordToFind = reference.replace(/\{\{|\}\}/g, '');
let homonymn = 0;
if (wordToFind.includes(':')) {
const separator = wordToFind.indexOf(':');
homonymn = wordToFind.substr(separator + 1);
wordToFind = wordToFind.substring(0, separator);
if (homonymn && homonymn.trim()
&& !isNaN(parseInt(homonymn.trim())) && parseInt(homonymn.trim()) > 0) {
homonymn = parseInt(homonymn.trim());
} else {
homonymn = false;
}
}
let existingWordId = false;
const homonymnIndexes = getHomonymnIndexes({ name: wordToFind, wordId: -1 });
if (homonymn !== false && homonymn > 0) {
if (typeof homonymnIndexes[homonymn - 1] !== 'undefined') {
existingWordId = window.currentDictionary.words[homonymnIndexes[homonymn - 1]].wordId;
}
} else if (homonymn !== false) {
existingWordId = wordExists(wordToFind, true);
}
if (existingWordId !== false) {
if (homonymn < 1 && homonymnIndexes.length > 0) {
homonymn = 1;
}
const homonymnSubHTML = homonymn > 0 ? '<sub>' + homonymn.toString() + '</sub>' : '';
const wordMarkdownLink = `[${wordToFind}${homonymnSubHTML}](#${existingWordId})`;
detailsMarkdown = detailsMarkdown.replace(new RegExp(reference, 'g'), wordMarkdownLink);
}
});
}
return detailsMarkdown;
}

View File

@ -1,14 +1,12 @@
RewriteEngine On # Turn on the rewriting engine
RewriteRule ^view/([0-9]+)/([0-9]+)/?$ router.php?view=word&dict=$1&word=$2 [NC,L] # Handle word ids.
RewriteRule ^view/([0-9]+)/([0-9]+)/?$ api/router.php?view=word&dict=$1&word=$2 [NC,L] # Handle word ids.
RewriteRule ^([0-9]+)/([0-9]+)/?$ router.php?view=word&dict=$1&word=$2 [NC,L] # Handle word ids.
RewriteRule ^([0-9]+)/([0-9]+)/?$ api/router.php?view=word&dict=$1&word=$2 [NC,L] # Handle word ids.
RewriteRule ^view/([0-9]+)/?$ router.php?view=dictionary&dict=$1 [NC,L] # Handle dictionary ids.
RewriteRule ^view/([0-9]+)/?$ api/router.php?view=dictionary&dict=$1 [NC,L] # Handle dictionary ids.
RewriteRule ^([0-9]+)/?$ router.php?view=dictionary&dict=$1 [NC,L] # Handle dictionary ids.
RewriteRule ^/?(index.html)?$ router.php [NC,L] # Handle dictionary ids.
RewriteRule ^([0-9]+)/?$ api/router.php?view=dictionary&dict=$1 [NC,L] # Handle dictionary ids.
#RewriteRule ^issues/?$ https://github.com/Alamantus/Lexiconga/issues [R=301,L] # Shorten issues url.

View File

@ -1,7 +0,0 @@
[
{
"header": "Test",
"body": "<p>Test</p>",
"expire": "January 1, 2020"
}
]

View File

@ -1,13 +1,12 @@
<?php
require_once(realpath(dirname(__FILE__) . '/./api/Response.php'));
$view = isset($_GET['view']) ? $_GET['view'] : false;
switch ($view) {
case 'dictionary': {
$html = file_get_contents(realpath(dirname(__FILE__) . '/./template-view.html'));
$html = file_get_contents('../template-view.html');
$dict = isset($_GET['dict']) ? $_GET['dict'] : false;
if ($dict !== false) {
require_once(realpath(dirname(__FILE__) . '/./api/Dictionary.php'));
require_once('./Dictionary.php');
$dictionary = new Dictionary();
$dictionary_data = $dictionary->getPublicDictionaryDetails($dict);
if ($dictionary_data !== false) {
@ -23,16 +22,16 @@ switch ($view) {
$html = str_replace('{{public_name}}', 'Error', $html);
$html = str_replace('{{dict_json}}', '{"name": "Error:", "specification": "Dictionary Not Found", "words": []}', $html);
}
return Response::html($html);
echo $html;
}
break;
}
case 'word': {
$html = file_get_contents(realpath(dirname(__FILE__) . '/./template-view.html'));
$html = file_get_contents('../template-view.html');
$dict = isset($_GET['dict']) ? $_GET['dict'] : false;
$word = isset($_GET['word']) ? $_GET['word'] : false;
if ($dict !== false && $word !== false) {
require_once(realpath(dirname(__FILE__) . '/./api/Dictionary.php'));
require_once('./Dictionary.php');
$dictionary = new Dictionary();
$dictionary_data = $dictionary->getPublicDictionaryDetails($dict);
if ($dictionary_data !== false) {
@ -62,69 +61,8 @@ switch ($view) {
$html = str_replace('{{public_name}}', 'Error', $html);
$html = str_replace('{{dict_json}}', '{"name": "Error:", "specification": "Dictionary Not Found", "words": []}', $html);
}
return Response::html($html);
echo $html;
}
break;
}
default: {
$html = file_get_contents(realpath(dirname(__FILE__) . '/./template-index.html'));
$announcements = file_get_contents(realpath(dirname(__FILE__) . '/./announcements.json'));
$announcements = json_decode($announcements, true);
$announcements_html = '';
foreach ($announcements as $announcement) {
$expire = strtotime($announcement['expire']);
if (time() < $expire) {
$announcements_html .= '<article class="announcement">
<a class="close-button" title="Close Announcement" onclick="this.parentElement.parentElement.removeChild(this.parentElement);">&times;&#xFE0E;</a>
<h4>' . $announcement['header'] . '</h4>
' . $announcement['body'] . '
</article>';
}
}
$html = str_replace('{{announcements}}', $announcements_html, $html);
$upup_files = array(
'src.js',
'main.css',
'help.html',
'privacy.html',
'terms.html',
'usage.html',
'ipa-table.html',
'favicon.png',
);
$files = array_map(function($file_name) use($upup_files) {
foreach($upup_files as $index => $upup_file) {
$file_pieces = explode('.', $upup_file);
if (substr($file_name, 0, strlen($file_pieces[0])) === $file_pieces[0]
&& substr($file_name, -strlen($file_pieces[1])) === $file_pieces[1]) {
return str_replace('\\', '/', $file_name);
}
}
return false;
}, scandir('.'));
$files = array_filter($files);
$upup_insert = "<script src=\"upup.min.js\"></script>
<script>
window.onload = (function (oldLoad) {
oldLoad && oldLoad();
if (UpUp) {
UpUp.start({
'cache-version': '2.0.0',
'content-url': 'offline.html',
'assets': [
\"" . implode('","', $files) . "\"
],
});
}
})(window.onload);
</script>";
$html = str_replace('{{upup_insert}}', $upup_insert, $html);
return Response::html($html);
break;
}
}

View File

@ -162,22 +162,6 @@
}
.announcement {
position: relative;
padding: 0 $general-padding $general-padding;
text-align: center;
.close-button {
position: absolute;
top: 5px;
right: 5px;
font-size: 25px;
line-height: 10px;
cursor: pointer;
text-decoration: none;
}
}
.entry {
.word {
display: inline-block;

View File

@ -182,11 +182,6 @@
}
}
.announcement {
background-color: saturate(lighten($footer-color, 20%), 20%);
box-shadow: 4px 4px 5px 0px #000000;
}
.entry {
background-color: $entry-color;
border: 1px solid darken($entry-color, 20);

View File

@ -182,11 +182,6 @@
}
}
.announcement {
background-color: saturate(lighten($footer-color, 20%), 20%);
box-shadow: 4px 4px 5px 0px $dark;
}
.entry {
background-color: $entry-color;
border: 1px solid darken($entry-color, 20);

View File

@ -181,11 +181,6 @@
}
}
.announcement {
background-color: saturate(lighten($footer-color, 20%), 20%);
box-shadow: 4px 4px 5px 0px rgba(50, 50, 50, 0.75);
}
.entry {
background-color: $entry-color;
border: 1px solid darken($entry-color, 20);

View File

@ -181,11 +181,6 @@
}
}
.announcement {
background-color: saturate(lighten($footer-color, 20%), 20%);
box-shadow: 4px 4px 5px 0px rgba(50, 50, 50, 0.75);
}
.entry {
background-color: $entry-color;
border: 1px solid darken($entry-color, 20);

View File

@ -182,11 +182,6 @@
}
}
.announcement {
background-color: saturate(lighten($footer-color, 20%), 20%);
box-shadow: 4px 4px 5px 0px #000000;
}
.entry {
background-color: $entry-color;
border: 1px solid darken($entry-color, 20);

View File

@ -181,11 +181,6 @@
}
}
.announcement {
background-color: saturate(lighten($footer-color, 20%), 20%);
box-shadow: 4px 4px 5px 0px rgba(50, 50, 50, 0.75);
}
.entry {
background-color: $entry-color;
border: 1px solid darken($entry-color, 20);

View File

@ -181,11 +181,6 @@
}
}
.announcement {
background-color: saturate(lighten($footer-color, 20%), 20%);
box-shadow: 4px 4px 5px 0px rgba(50, 50, 50, 0.75);
}
.entry {
background-color: $entry-color;
border: 1px solid darken($entry-color, 20);

View File

@ -182,11 +182,6 @@
}
}
.announcement {
background-color: saturate(lighten($footer-color, 20%), 20%);
box-shadow: 4px 4px 5px 0px #000000;
}
.entry {
background-color: $entry-color;
border: 1px solid darken($entry-color, 20);

View File

@ -182,11 +182,6 @@
}
}
.announcement {
background-color: saturate(lighten($footer-color, 20%), 20%);
box-shadow: 4px 4px 5px 0px #000000;
}
.entry {
background-color: $entry-color;
border: 1px solid darken($entry-color, 20);

View File

@ -182,11 +182,6 @@
}
}
.announcement {
background-color: saturate(lighten($footer-color, 20%), 20%);
box-shadow: 4px 4px 5px 0px #000000;
}
.entry {
background-color: $entry-color;
border: 1px solid darken($entry-color, 20);