From 910e0259978dae5f3a5c6e3257dff148d508cd31 Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Tue, 28 May 2019 17:01:29 -0600 Subject: [PATCH 001/134] Make public link visible when public; copy on click --- src/js/account/render.js | 12 ++++++++++-- src/js/account/setupListeners.js | 12 ++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/js/account/render.js b/src/js/account/render.js index 38f2d92..d391e63 100644 --- a/src/js/account/render.js +++ b/src/js/account/render.js @@ -1,4 +1,4 @@ -import { setupLoginModal, setupChangeDictionary, setupCreateNewDictionary, setupDeletedDictionaryChangeModal } from "./setupListeners"; +import { setupLoginModal, setupChangeDictionary, setupCreateNewDictionary, setupDeletedDictionaryChangeModal, setupMakePublic } from "./setupListeners"; import { request } from "./helpers"; export function renderLoginForm() { @@ -55,12 +55,20 @@ export function renderLoginForm() { export function renderMakePublic() { const editSettingsTab = document.getElementById('editSettingsTab'); + const { isPublic } = window.currentDictionary.settings; const editSettingsTabHTML = ` +

+ Public Link:
+ + Copy +

`; editSettingsTab.innerHTML += editSettingsTabHTML; + + setupMakePublic(); } export function renderAccountSettings() { diff --git a/src/js/account/setupListeners.js b/src/js/account/setupListeners.js index 60670d4..b3bf316 100644 --- a/src/js/account/setupListeners.js +++ b/src/js/account/setupListeners.js @@ -1,6 +1,7 @@ import { logIn, createAccount } from "./login"; import { setCookie } from "../StackOverflow/cookie"; import { changeDictionary, createNewDictionary } from "./dictionaryManagement"; +import { addMessage } from "../utilities"; export function setupLoginModal(modal) { const closeElements = modal.querySelectorAll('.modal-background, .close-button'); @@ -60,3 +61,14 @@ export function setupDeletedDictionaryChangeModal() { } document.getElementById('createNewDictionaryAfterDelete').addEventListener('click', createNewDictionary); } + +export function setupMakePublic() { + document.getElementById('editIsPublic').addEventListener('change', function(event) { + document.getElementById('publicLinkDisplay').style.display = event.target.checked ? '' : 'none'; + }); + document.getElementById('publicLinkCopy').addEventListener('click', function() { + document.getElementById('publicLink').select(); + document.execCommand('copy'); + addMessage('Copied public link to clipboard', 3000); + }); +} From 50b094122384999f6a2d847f75aff647bcf245b3 Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Tue, 28 May 2019 22:17:26 -0600 Subject: [PATCH 002/134] Prevent iOS from zooming on inputs --- src/main.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main.scss b/src/main.scss index a0c66e6..b8f15c8 100644 --- a/src/main.scss +++ b/src/main.scss @@ -15,4 +15,10 @@ html, body { * { box-sizing: border-box; } +} + +input:not([type="checkbox"]), +select, +textarea { + font-size: 16px; } \ No newline at end of file From 4bdeff3296f2d46209b6094d1384f578b3b2e41e Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Tue, 28 May 2019 22:23:59 -0600 Subject: [PATCH 003/134] Add error message if public dictionary not found --- src/php/api/router.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/php/api/router.php b/src/php/api/router.php index 25f16ff..b81d896 100644 --- a/src/php/api/router.php +++ b/src/php/api/router.php @@ -1,10 +1,10 @@ Date: Tue, 28 May 2019 23:12:51 -0600 Subject: [PATCH 004/134] Make sure publicLink is populated even when nonexistent --- src/js/account/render.js | 3 ++- src/js/account/sync.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/js/account/render.js b/src/js/account/render.js index d391e63..c74b118 100644 --- a/src/js/account/render.js +++ b/src/js/account/render.js @@ -56,13 +56,14 @@ export function renderLoginForm() { export function renderMakePublic() { const editSettingsTab = document.getElementById('editSettingsTab'); const { isPublic } = window.currentDictionary.settings; + const { externalID } = window.currentDictionary; const editSettingsTabHTML = `

Public Link:
- + Copy

`; diff --git a/src/js/account/sync.js b/src/js/account/sync.js index 8f2e1ee..37bda23 100644 --- a/src/js/account/sync.js +++ b/src/js/account/sync.js @@ -100,6 +100,7 @@ export function uploadWholeDictionary(asNew = false) { dictionary, }, remoteId => { window.currentDictionary.externalID = remoteId; + dictionary.getElementById('publicLink').value = window.location.href + remoteId.toString(); saveDictionary(false); addMessage('Dictionary Uploaded Successfully'); renderChangeDictionaryOptions(); From 520ede111bee17635a1d4f9b612f46748e2a0618 Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Tue, 28 May 2019 23:45:32 -0600 Subject: [PATCH 005/134] Auto-number homonymns; enable referencing specific homonymns --- src/js/render.js | 16 +++++----------- src/js/utilities.js | 29 +++++++++++++++++++++++++++++ src/js/wordManagement.js | 38 +++++++++++++++++++++++++++++++++++++- src/markdown/help.md | 3 ++- 4 files changed, 73 insertions(+), 13 deletions(-) diff --git a/src/js/render.js b/src/js/render.js index 5aba0f8..afbb7ab 100644 --- a/src/js/render.js +++ b/src/js/render.js @@ -1,6 +1,6 @@ import md from 'marked'; import { removeTags, slugify } from '../helpers'; -import { getWordsStats, wordExists } from './utilities'; +import { getWordsStats, getHomonymnNumber } from './utilities'; import { getMatchingSearchWords, highlightSearchTerm, getSearchFilters, getSearchTerm } from './search'; import { showSection } from './displayToggles'; import { @@ -15,7 +15,7 @@ import { setupIPAFields } from './setupListeners'; import { getPaginationData } from './pagination'; -import { getOpenEditForms } from './wordManagement'; +import { getOpenEditForms, parseReferences } from './wordManagement'; export function renderAll() { renderDictionaryDetails(); @@ -160,14 +160,7 @@ export function renderWords() { let detailsMarkdown = removeTags(originalWord.details); const references = detailsMarkdown.match(/\{\{.+?\}\}/g); if (references && Array.isArray(references)) { - new Set(references).forEach(reference => { - const wordToFind = reference.replace(/\{\{|\}\}/g, ''); - const existingWordId = wordExists(wordToFind, true); - if (existingWordId !== false) { - const wordMarkdownLink = `[${wordToFind}](#${existingWordId})`; - detailsMarkdown = detailsMarkdown.replace(new RegExp(reference, 'g'), wordMarkdownLink); - } - }); + detailsMarkdown = parseReferences(detailsMarkdown, references); } const word = highlightSearchTerm({ name: removeTags(originalWord.name), @@ -177,9 +170,10 @@ export function renderWords() { details: detailsMarkdown, wordId: originalWord.wordId, }); + const homonymnNumber = getHomonymnNumber(originalWord); wordsHTML += `
-

${word.name}

+

${word.name}${homonymnNumber > 0 ? ' ' + homonymnNumber.toString() + '' : ''}

${word.pronunciation} ${word.partOfSpeech} Options diff --git a/src/js/utilities.js b/src/js/utilities.js index cfa4e2d..3e2444a 100644 --- a/src/js/utilities.js +++ b/src/js/utilities.js @@ -107,6 +107,35 @@ export function wordExists(word, returnId = false) { return foundWord ? (returnId ? foundWord.wordId : true) : false; } +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; +} + export function generateRandomWords(numberOfWords) { console.log('Generating', numberOfWords, 'words...'); const letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']; diff --git a/src/js/wordManagement.js b/src/js/wordManagement.js index faad17b..3a2a3c5 100644 --- a/src/js/wordManagement.js +++ b/src/js/wordManagement.js @@ -1,5 +1,5 @@ import { renderWords } from "./render"; -import { wordExists, addMessage, getNextId, hasToken } from "./utilities"; +import { wordExists, addMessage, getNextId, hasToken, getHomonymnIndexes } from "./utilities"; import removeDiacritics from "./StackOverflow/removeDiacritics"; import { removeTags, getTimestampInSeconds } from "../helpers"; import { saveDictionary } from "./dictionaryManagement"; @@ -46,6 +46,42 @@ export function sortWords(render) { } } +export function parseReferences(detailsMarkdown, 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()))) { + homonymn = parseInt(homonymn.trim()); + } else { + homonymn = 0; + } + } + let existingWordId = false; + const homonymnIndexes = getHomonymnIndexes({ name: wordToFind, wordId: -1 }); + console.log(homonymn, homonymnIndexes); + if (homonymn > 0) { + if (typeof homonymnIndexes[homonymn - 1] !== 'undefined') { + existingWordId = window.currentDictionary.words[homonymnIndexes[homonymn - 1]].wordId; + } + } else { + existingWordId = wordExists(wordToFind, true); + } + if (existingWordId !== false) { + if (homonymn < 1 && homonymnIndexes.length > 0) { + homonymn = 1; + } + const homonymnSubHTML = homonymn > 0 ? '' + homonymn.toString() + '' : ''; + const wordMarkdownLink = `[${wordToFind}${homonymnSubHTML}](#${existingWordId})`; + detailsMarkdown = detailsMarkdown.replace(new RegExp(reference, 'g'), wordMarkdownLink); + } + }); + return detailsMarkdown; +} + export function submitWordForm() { const name = document.getElementById('wordName').value, pronunciation = document.getElementById('wordPronunciation').value, diff --git a/src/markdown/help.md b/src/markdown/help.md index 8eaada0..96f6a27 100644 --- a/src/markdown/help.md +++ b/src/markdown/help.md @@ -54,7 +54,8 @@ After you enter a markdown-formatted description/rules in the Dictionary Setting ### Referencing Other Words If you want to reference another existing word in your dictionary, wrapping the word with its exact name in double-curly-braces \{\{like so\}\} in the Details field will automatically create a link to the word in the dictionary. -Note: If you do not prevent duplicates, it will find the first entry in the list. + +If you have more than one word with the same spelling, the duplicate words will appear in your word listing with small numbers beside them. By writing the number after a colon \{\{like so:2\}\}, you can directly reference the specific homonymn. If you have duplicate words and exclude the reference number, it will link to the homonymn marked with 1. ### Maximizing Large Text Boxes If you need more space to see what you are entering into a word's Details field or any other long text field with a "Maximize" button, clicking "Maximize" will give you a larger view of the text box to enter text in. When you're done writing, click either the "Done" or × button or any of the darker space outside of the larger view, and your text will be in the original text area. It will even preserve your cursor position or highlighted text so you don't lose your place moving from the larger view back to the small (and vice-versa)! From b59c4702b245412961326d14e1384847ba083678 Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Wed, 29 May 2019 00:01:59 -0600 Subject: [PATCH 006/134] Use domain + pathname for public links instead of href --- src/js/account/render.js | 2 +- src/js/account/sync.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/account/render.js b/src/js/account/render.js index c74b118..b5d7046 100644 --- a/src/js/account/render.js +++ b/src/js/account/render.js @@ -63,7 +63,7 @@ export function renderMakePublic() {

Public Link:
- + Copy

`; diff --git a/src/js/account/sync.js b/src/js/account/sync.js index 37bda23..5b4df0b 100644 --- a/src/js/account/sync.js +++ b/src/js/account/sync.js @@ -100,7 +100,7 @@ export function uploadWholeDictionary(asNew = false) { dictionary, }, remoteId => { window.currentDictionary.externalID = remoteId; - dictionary.getElementById('publicLink').value = window.location.href + remoteId.toString(); + dictionary.getElementById('publicLink').value = document.domain + window.location.pathname + remoteId.toString(); saveDictionary(false); addMessage('Dictionary Uploaded Successfully'); renderChangeDictionaryOptions(); From 4a1dd7aae410b6f8a04633e93382dad2aed3a152 Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Wed, 29 May 2019 00:02:22 -0600 Subject: [PATCH 007/134] Add share links to words (word view not added yet) --- src/js/render.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/js/render.js b/src/js/render.js index afbb7ab..fa15333 100644 --- a/src/js/render.js +++ b/src/js/render.js @@ -1,6 +1,6 @@ import md from 'marked'; import { removeTags, slugify } from '../helpers'; -import { getWordsStats, getHomonymnNumber } from './utilities'; +import { getWordsStats, getHomonymnNumber, hasToken } from './utilities'; import { getMatchingSearchWords, highlightSearchTerm, getSearchFilters, getSearchTerm } from './search'; import { showSection } from './displayToggles'; import { @@ -122,6 +122,7 @@ export function renderWords() { let wordsHTML = ''; let openEditForms = getOpenEditForms(); let words = false; + const isPublic = hasToken() && window.currentDictionary.settings.isPublic; if (window.currentDictionary.words.length === 0) { wordsHTML = `
@@ -171,6 +172,8 @@ export function renderWords() { wordId: originalWord.wordId, }); const homonymnNumber = getHomonymnNumber(originalWord); + const shareLink = window.currentDictionary.hasOwnProperty('externalID') + ? window.location.pathname + window.currentDictionary.externalID + '/' + word.wordId : ''; wordsHTML += `

${word.name}${homonymnNumber > 0 ? ' ' + homonymnNumber.toString() + '' : ''}

@@ -179,6 +182,7 @@ export function renderWords() { Options
From 08cadfb121b66650360eb445cba5b50d18a9f140 Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Wed, 29 May 2019 15:36:44 -0600 Subject: [PATCH 008/134] Prevent rendering reference if specified does not exist --- src/js/wordManagement.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/js/wordManagement.js b/src/js/wordManagement.js index 3a2a3c5..2e55dec 100644 --- a/src/js/wordManagement.js +++ b/src/js/wordManagement.js @@ -50,26 +50,30 @@ export function parseReferences(detailsMarkdown, 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()))) { + if (homonymn && homonymn.trim() + && !isNaN(parseInt(homonymn.trim())) && parseInt(homonymn.trim()) > 0) { homonymn = parseInt(homonymn.trim()); } else { - homonymn = 0; + homonymn = false; } } + let existingWordId = false; const homonymnIndexes = getHomonymnIndexes({ name: wordToFind, wordId: -1 }); - console.log(homonymn, homonymnIndexes); - if (homonymn > 0) { + + if (homonymn !== false && homonymn > 0) { if (typeof homonymnIndexes[homonymn - 1] !== 'undefined') { existingWordId = window.currentDictionary.words[homonymnIndexes[homonymn - 1]].wordId; } - } else { + } else if (homonymn !== false) { existingWordId = wordExists(wordToFind, true); } + if (existingWordId !== false) { if (homonymn < 1 && homonymnIndexes.length > 0) { homonymn = 1; From 63b1f20d5811441cfe53d88f74a8951828ee7a55 Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Wed, 29 May 2019 15:47:03 -0600 Subject: [PATCH 009/134] Fix login error message --- src/js/account/login.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/account/login.js b/src/js/account/login.js index 7c6fefd..6eed4a2 100644 --- a/src/js/account/login.js +++ b/src/js/account/login.js @@ -30,9 +30,9 @@ export function logIn() { saveToken(successData.token); window.account = successData.user; }, errorData => { - errorHTML += errorData; + errorHTML += '

' + errorData + '

'; }).then(() => { - createAccountErrorMessages.innerHTML = errorHTML; + loginErrorMessages.innerHTML = errorHTML; if (errorHTML === '') { const loginModal = document.getElementById('loginModal'); loginModal.parentElement.removeChild(loginModal); From dafecd9582385d200ca293be4eb761f92d4d82fa Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Wed, 29 May 2019 16:35:51 -0600 Subject: [PATCH 010/134] Add failed login lockout --- src/php/api/config.php.changeme | 5 ++++- src/php/api/index.php | 36 ++++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/php/api/config.php.changeme b/src/php/api/config.php.changeme index b2aa988..99d1109 100644 --- a/src/php/api/config.php.changeme +++ b/src/php/api/config.php.changeme @@ -1,4 +1,7 @@ 'Too many failed login attempts. You must wait another ' + . ($minutes_left > 0 ? $minutes_left . ' minutes ' : '') + . ($minutes_left > 0 && $seconds_left > 0 ? 'and ' : '') + . ($seconds_left > 0 ? $seconds_left . ' seconds ' : '') + . 'until you can log in again.', + 'error' => true, + ), 403); + } else { + unset($_SESSION['failures']); + unset($_SESSION['unlock']); + } + } + if (isset($request['email']) && isset($request['password'])) { $user = new User(); $user_data = $user->logIn($request['email'], $request['password']); @@ -48,8 +68,22 @@ switch ($action) { 'error' => false, ), 200); } + + if (!isset($_SESSION['failures'])) { + $_SESSION['failures'] = 0; + } + $_SESSION['failures']++; + + if ($_SESSION['failures'] >= LOGIN_FAILURES_ALLOWED) { + $_SESSION['unlock'] = time() + (LOGIN_FAILURES_LOCKOUT_MINUTES * 60); + return Response::json(array( + 'data' => 'Too many failed login attempts. You must wait ' . LOGIN_FAILURES_LOCKOUT_MINUTES . ' minutes until you can log in again.', + 'error' => true, + ), 403); + } + return Response::json(array( - 'data' => 'Could not log in: incorrect data', + 'data' => 'Incorrect email or password.
After ' . (LOGIN_FAILURES_ALLOWED - $_SESSION['failures']) . ' more failures, you will be locked out for ' . LOGIN_FAILURES_LOCKOUT_MINUTES . ' minutes.', 'error' => true, ), 401); } From 19e41958a4230745a96ef0e0ef3998cdf438aad0 Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Thu, 30 May 2019 11:18:16 -0600 Subject: [PATCH 011/134] Make parseReferences() parse its own references --- src/js/wordManagement.js | 65 +++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/src/js/wordManagement.js b/src/js/wordManagement.js index 2e55dec..88239fa 100644 --- a/src/js/wordManagement.js +++ b/src/js/wordManagement.js @@ -46,43 +46,46 @@ export function sortWords(render) { } } -export function parseReferences(detailsMarkdown, 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; +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 }); + 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; + 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); } - } else if (homonymn !== false) { - existingWordId = wordExists(wordToFind, true); - } - if (existingWordId !== false) { - if (homonymn < 1 && homonymnIndexes.length > 0) { - homonymn = 1; + if (existingWordId !== false) { + if (homonymn < 1 && homonymnIndexes.length > 0) { + homonymn = 1; + } + const homonymnSubHTML = homonymn > 0 ? '' + homonymn.toString() + '' : ''; + const wordMarkdownLink = `[${wordToFind}${homonymnSubHTML}](#${existingWordId})`; + detailsMarkdown = detailsMarkdown.replace(new RegExp(reference, 'g'), wordMarkdownLink); } - const homonymnSubHTML = homonymn > 0 ? '' + homonymn.toString() + '' : ''; - const wordMarkdownLink = `[${wordToFind}${homonymnSubHTML}](#${existingWordId})`; - detailsMarkdown = detailsMarkdown.replace(new RegExp(reference, 'g'), wordMarkdownLink); - } - }); + }); + } return detailsMarkdown; } From 1872fffba87dae9703fc6d4a43d0be5a11bd5923 Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Thu, 30 May 2019 11:20:02 -0600 Subject: [PATCH 012/134] Add public word view --- src/php/.htaccess | 8 ++++---- src/php/api/Dictionary.php | 20 +++++++++++++++++++ src/php/api/router.php | 41 +++++++++++++++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/php/.htaccess b/src/php/.htaccess index 790c499..43599f9 100644 --- a/src/php/.htaccess +++ b/src/php/.htaccess @@ -1,12 +1,12 @@ RewriteEngine On # Turn on the rewriting engine -RewriteRule ^view/([0-9]+)/([0-9]+)/?$ api/router.php?view=publicview&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]+)/?$ api/router.php?view=publicview&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]+)/?$ api/router.php?view=publicview&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]+)/?$ api/router.php?view=publicview&dict=$1 [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. diff --git a/src/php/api/Dictionary.php b/src/php/api/Dictionary.php index a1bc1c3..f3e3f66 100644 --- a/src/php/api/Dictionary.php +++ b/src/php/api/Dictionary.php @@ -160,6 +160,26 @@ VALUES ($new_id, ?, ?, ?, ?)"; return array(); } + public function getSpecificPublicDictionaryWord ($dictionary, $word) { + if (is_numeric($dictionary) && is_numeric($word)) { + $query = "SELECT words.* FROM words JOIN dictionaries ON id = dictionary WHERE dictionary=? AND word_id=? AND is_public=1"; + $result = $this->db->query($query, array($dictionary, $word))->fetch(); + if ($result) { + return array( + 'name' => $result['name'], + 'pronunciation' => $result['pronunciation'], + 'partOfSpeech' => $result['part_of_speech'], + 'definition' => $result['definition'], + 'details' => $result['details'], + 'lastUpdated' => is_null($result['last_updated']) ? intval($result['created_on']) : intval($result['last_updated']), + 'createdOn' => intval($result['created_on']), + 'wordId' => intval($result['word_id']), + ); + } + } + return false; + } + public function getDetails ($user, $dictionary) { $query = "SELECT * FROM dictionaries JOIN dictionary_linguistics ON dictionary = id WHERE user=$user AND id=$dictionary"; $result = $this->db->query($query)->fetch(); diff --git a/src/php/api/router.php b/src/php/api/router.php index b81d896..4c5e3c9 100644 --- a/src/php/api/router.php +++ b/src/php/api/router.php @@ -2,7 +2,7 @@ $view = isset($_GET['view']) ? $_GET['view'] : false; switch ($view) { - case 'publicview': { + case 'dictionary': { $html = file_get_contents('../view.html'); $dict = isset($_GET['dict']) ? $_GET['dict'] : false; if ($dict !== false) { @@ -26,4 +26,43 @@ switch ($view) { } break; } + case 'word': { + $html = file_get_contents('../view.html'); + $dict = isset($_GET['dict']) ? $_GET['dict'] : false; + $word = isset($_GET['word']) ? $_GET['word'] : false; + if ($dict !== false && $word !== false) { + require_once('./Dictionary.php'); + $dictionary = new Dictionary(); + $dictionary_data = $dictionary->getPublicDictionaryDetails($dict); + if ($dictionary_data !== false) { + $dictionary_name = $dictionary_data['name'] . ' ' . $dictionary_data['specification']; + $word_data = $dictionary->getSpecificPublicDictionaryWord($dict, $word); + if ($word_data === false) { + $word_data = array( + 'name' => 'Error: Word Not Found', + 'pronunciation' => '', + 'partOfSpeech' => '', + 'definition' => 'No word with the id ' . $word . ' was found in the ' . $dictionary_name, + 'details' => '', + 'lastUpdated' => null, + 'createdOn' => null, + 'wordId' => null, + ); + } + $dictionary_data['words'] = array($word_data); + $html = str_replace('{{dict}}', $dict, $html); + $html = str_replace('{{dict_name}}', $word_data['name'] . ' in the ' . $dictionary_name, $html); + $html = str_replace('{{public_name}}', $dictionary_data['createdBy'], $html); + $dictionary_json = json_encode($dictionary_data); + $html = str_replace('{{dict_json}}', addslashes($dictionary_json), $html); + } else { + $html = str_replace('{{dict}}', 'error', $html); + $html = str_replace('{{dict_name}}', 'Error: Dictionary Not Found', $html); + $html = str_replace('{{public_name}}', 'Error', $html); + $html = str_replace('{{dict_json}}', '{"name": "Error:", "specification": "Dictionary Not Found", "words": []}', $html); + } + echo $html; + } + break; + } } \ No newline at end of file From eb0dd669bbc5550732c0460f79520968beb8ca1e Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Thu, 30 May 2019 12:18:59 -0600 Subject: [PATCH 013/134] Correctly link public word references --- src/php/api/Dictionary.php | 62 +++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/src/php/api/Dictionary.php b/src/php/api/Dictionary.php index f3e3f66..924a550 100644 --- a/src/php/api/Dictionary.php +++ b/src/php/api/Dictionary.php @@ -165,12 +165,13 @@ VALUES ($new_id, ?, ?, ?, ?)"; $query = "SELECT words.* FROM words JOIN dictionaries ON id = dictionary WHERE dictionary=? AND word_id=? AND is_public=1"; $result = $this->db->query($query, array($dictionary, $word))->fetch(); if ($result) { + $details = $this->parseReferences($result['details'], $dictionary); return array( 'name' => $result['name'], 'pronunciation' => $result['pronunciation'], 'partOfSpeech' => $result['part_of_speech'], 'definition' => $result['definition'], - 'details' => $result['details'], + 'details' => $details, 'lastUpdated' => is_null($result['last_updated']) ? intval($result['created_on']) : intval($result['last_updated']), 'createdOn' => intval($result['created_on']), 'wordId' => intval($result['word_id']), @@ -180,6 +181,65 @@ VALUES ($new_id, ?, ?, ?, ?)"; return false; } + private function parseReferences($details, $dictionary_id) { + if (preg_match_all('/\{\{.+?\}\}/', $details, $references) !== false) { + $references = array_unique($references[0]); + foreach($references as $reference) { + $word_to_find = preg_replace('/\{\{|\}\}/', '', $reference); + $homonymn = 0; + + if (strpos($word_to_find, ':') !== false) { + $separator = strpos($word_to_find, ':'); + $homonymn = substr($word_to_find, $separator + 1); + $word_to_find = substr($word_to_find, 0, $separator); + if ($homonymn && trim($homonymn) && intval(trim($homonymn)) > 0) { + $homonymn = intval(trim($homonymn)); + } else { + $homonymn = false; + } + } + + $target_id = false; + $reference_ids = $this->getWordIdsWithName($dictionary_id, $word_to_find); + + if (count($reference_ids) > 0) { + if ($homonymn !== false && $homonymn > 0) { + if (isset($reference_ids[$homonymn - 1])) { + $target_id = $reference_ids[$homonymn - 1]; + } + } else if ($homonymn !== false) { + $target_id = $reference_ids[0]; + } + + if ($target_id !== false) { + if ($homonymn < 1) { + $homonymn = 1; + } + $homonymn_sub_html = $homonymn > 0 ? '' . $homonymn . '' : ''; + $site_root = substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], $dictionary_id)); + $markdown_link = '[' . $word_to_find . $homonymn_sub_html . '](' . $site_root . $dictionary_id . '/' . $target_id . ')'; + $details = str_replace($reference, $markdown_link, $details); + } + } + } + } + + return $details; + } + + private function getWordIdsWithName($dictionary, $word_name) { + if (is_numeric($dictionary)) { + $query = "SELECT word_id FROM words WHERE dictionary=? AND name=?"; + $results = $this->db->query($query, array($dictionary, $word_name))->fetchAll(); + if ($results) { + return array_map(function ($row) { + return intval($row['word_id']); + }, $results); + } + } + return array(); + } + public function getDetails ($user, $dictionary) { $query = "SELECT * FROM dictionaries JOIN dictionary_linguistics ON dictionary = id WHERE user=$user AND id=$dictionary"; $result = $this->db->query($query)->fetch(); From e9cf9653be702c4bd05d28b9c73df966c1708a63 Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Thu, 30 May 2019 12:32:44 -0600 Subject: [PATCH 014/134] Parse references on backend for view --- src/js/view/render.js | 15 +++------------ src/php/api/Dictionary.php | 11 ++++++----- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/js/view/render.js b/src/js/view/render.js index 7082d23..6c1b86e 100644 --- a/src/js/view/render.js +++ b/src/js/view/render.js @@ -4,6 +4,7 @@ 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'; export function renderAll() { renderDictionaryDetails(); @@ -134,18 +135,8 @@ export function renderWords() { } words.forEach(originalWord => { - let detailsMarkdown = removeTags(originalWord.details); - const references = detailsMarkdown.match(/\{\{.+?\}\}/g); - if (references && Array.isArray(references)) { - new Set(references).forEach(reference => { - const wordToFind = reference.replace(/\{\{|\}\}/g, ''); - const existingWordId = wordExists(wordToFind, true); - if (existingWordId !== false) { - const wordMarkdownLink = `[${wordToFind}](#${existingWordId})`; - detailsMarkdown = detailsMarkdown.replace(new RegExp(reference, 'g'), wordMarkdownLink); - } - }); - } + let detailsMarkdown = originalWord.details; + detailsMarkdown = parseReferences(detailsMarkdown); const word = highlightSearchTerm({ name: removeTags(originalWord.name), pronunciation: removeTags(originalWord.pronunciation), diff --git a/src/php/api/Dictionary.php b/src/php/api/Dictionary.php index 924a550..485d32d 100644 --- a/src/php/api/Dictionary.php +++ b/src/php/api/Dictionary.php @@ -143,13 +143,13 @@ VALUES ($new_id, ?, ?, ?, ?)"; $query = "SELECT words.* FROM words JOIN dictionaries ON id = dictionary WHERE dictionary=? AND is_public=1"; $results = $this->db->query($query, array($dictionary))->fetchAll(); if ($results) { - return array_map(function ($row) { + return array_map(function ($row) use ($dictionary) { return array( 'name' => $row['name'], 'pronunciation' => $row['pronunciation'], 'partOfSpeech' => $row['part_of_speech'], 'definition' => $row['definition'], - 'details' => $row['details'], + 'details' => $this->parseReferences($row['details'], $dictionary), 'lastUpdated' => is_null($row['last_updated']) ? intval($row['created_on']) : intval($row['last_updated']), 'createdOn' => intval($row['created_on']), 'wordId' => intval($row['word_id']), @@ -165,13 +165,12 @@ VALUES ($new_id, ?, ?, ?, ?)"; $query = "SELECT words.* FROM words JOIN dictionaries ON id = dictionary WHERE dictionary=? AND word_id=? AND is_public=1"; $result = $this->db->query($query, array($dictionary, $word))->fetch(); if ($result) { - $details = $this->parseReferences($result['details'], $dictionary); return array( 'name' => $result['name'], 'pronunciation' => $result['pronunciation'], 'partOfSpeech' => $result['part_of_speech'], 'definition' => $result['definition'], - 'details' => $details, + 'details' => $this->parseReferences($result['details'], $dictionary), 'lastUpdated' => is_null($result['last_updated']) ? intval($result['created_on']) : intval($result['last_updated']), 'createdOn' => intval($result['created_on']), 'wordId' => intval($result['word_id']), @@ -182,6 +181,7 @@ VALUES ($new_id, ?, ?, ?, ?)"; } private function parseReferences($details, $dictionary_id) { + $details = strip_tags($details); if (preg_match_all('/\{\{.+?\}\}/', $details, $references) !== false) { $references = array_unique($references[0]); foreach($references as $reference) { @@ -217,7 +217,8 @@ VALUES ($new_id, ?, ?, ?, ?)"; } $homonymn_sub_html = $homonymn > 0 ? '' . $homonymn . '' : ''; $site_root = substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], $dictionary_id)); - $markdown_link = '[' . $word_to_find . $homonymn_sub_html . '](' . $site_root . $dictionary_id . '/' . $target_id . ')'; + $markdown_link = '' + . $word_to_find . $homonymn_sub_html . ''; $details = str_replace($reference, $markdown_link, $details); } } From ce143be3f5a6f3ed1eefa1d82d0e5cf8383fcc75 Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Thu, 30 May 2019 14:01:34 -0600 Subject: [PATCH 015/134] Add prominent share links if logged in or viewing --- src/js/render.js | 18 ++++++++++++++++-- src/js/view/render.js | 5 +++++ src/scss/Account/_structure.scss | 6 ++++++ view.html | 4 +++- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/js/render.js b/src/js/render.js index fa15333..8c7f573 100644 --- a/src/js/render.js +++ b/src/js/render.js @@ -36,7 +36,21 @@ export function renderDictionaryDetails() { export function renderName() { const dictionaryName = removeTags(window.currentDictionary.name) + ' ' + removeTags(window.currentDictionary.specification); - document.getElementById('dictionaryName').innerHTML = dictionaryName; + const name = document.getElementById('dictionaryName'); + name.innerHTML = dictionaryName; + const isPublic = hasToken() && window.currentDictionary.settings.isPublic; + if (isPublic && !document.getElementById('dictionaryShare')) { + const shareLink = document.createElement('a'); + shareLink.id = 'dictionaryShare'; + shareLink.classList.add('button'); + shareLink.style.float = 'right'; + shareLink.href = window.location.pathname.match(new RegExp(window.currentDictionary.externalID + '$')) ? window.location.pathname + : window.location.pathname.substring(0, window.location.pathname.indexOf(window.currentDictionary.externalID)) + window.currentDictionary.externalID; + shareLink.target = '_blank'; + shareLink.title = 'Public Link to Dictionary'; + shareLink.innerHTML = '➦'; + name.parentElement.insertBefore(shareLink, name); + } } export function renderDescription() { @@ -179,10 +193,10 @@ export function renderWords() {

${word.name}${homonymnNumber > 0 ? ' ' + homonymnNumber.toString() + '' : ''}

${word.pronunciation} ${word.partOfSpeech} + ${isPublic ? `` : ''} Options
diff --git a/src/js/view/render.js b/src/js/view/render.js index 6c1b86e..2d8ed15 100644 --- a/src/js/view/render.js +++ b/src/js/view/render.js @@ -26,6 +26,9 @@ export function renderDictionaryDetails() { export function renderName() { const dictionaryName = removeTags(window.currentDictionary.name) + ' ' + removeTags(window.currentDictionary.specification); document.getElementById('dictionaryName').innerHTML = dictionaryName; + const shareLink = window.location.pathname.match(new RegExp(window.currentDictionary.externalID + '$')) ? window.location.pathname + : window.location.pathname.substring(0, window.location.pathname.indexOf(window.currentDictionary.externalID)) + window.currentDictionary.externalID; + document.getElementById('dictionaryShare').href = shareLink; } export function renderDescription() { @@ -145,11 +148,13 @@ export function renderWords() { details: detailsMarkdown, wordId: originalWord.wordId, }); + const shareLink = window.location.pathname + (window.location.pathname.match(new RegExp(word.wordId + '$')) ? '' : '/' + word.wordId); wordsHTML += `

${word.name}

${word.pronunciation} ${word.partOfSpeech} +
${word.definition}
diff --git a/src/scss/Account/_structure.scss b/src/scss/Account/_structure.scss index e54f47f..2009354 100644 --- a/src/scss/Account/_structure.scss +++ b/src/scss/Account/_structure.scss @@ -13,4 +13,10 @@ } } } +} + +.share-link { + margin-left: $general-padding !important; + line-height: 16px !important; + padding: 1px 3px 3px !important; } \ No newline at end of file diff --git a/view.html b/view.html index ae2b342..b0300ed 100644 --- a/view.html +++ b/view.html @@ -109,7 +109,9 @@
-

Dictionary Name

+ +

{{dict_name}}

+

Created by {{public_name}}

From 4c5dafd6f0e65de6a4f01aab9412a366c7f5a1a3 Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Sat, 6 Jul 2019 22:40:58 -0600 Subject: [PATCH 104/134] Improve Details display of tags --- src/js/render.js | 46 +++++++++++++++++++++++-------------------- src/js/view/render.js | 44 ++++++++++++++++++++++------------------- 2 files changed, 49 insertions(+), 41 deletions(-) diff --git a/src/js/render.js b/src/js/render.js index b0afd82..e3a0e20 100644 --- a/src/js/render.js +++ b/src/js/render.js @@ -76,16 +76,16 @@ export function renderDescription() { export function renderDetails() { const { partsOfSpeech, alphabeticalOrder } = window.currentDictionary; const { phonology, phonotactics, orthography, grammar } = window.currentDictionary.details; - const partsOfSpeechHTML = `

Parts of Speech: ${partsOfSpeech.map(partOfSpeech => '' + partOfSpeech + '').join(' ')}

`; - const alphabeticalOrderHTML = `

Alphabetical Order: ${ + const partsOfSpeechHTML = `

Parts of Speech
${partsOfSpeech.map(partOfSpeech => '' + partOfSpeech + '').join(' ')}`; + const alphabeticalOrderHTML = `

Alphabetical Order
${ (alphabeticalOrder.length > 0 ? alphabeticalOrder : ['English Alphabet']).map(letter => `${letter}`).join(' ') - }

`; + }`; const generalHTML = `

General

${partsOfSpeechHTML}${alphabeticalOrderHTML}`; const { consonants, vowels, blends } = phonology - const consonantHTML = `

Consonants: ${consonants.map(letter => `${letter}`).join(' ')}

`; - const vowelHTML = `

Vowels: ${vowels.map(letter => `${letter}`).join(' ')}

`; - const blendHTML = blends.length > 0 ? `

Polyphthongs / Blends: ${blends.map(letter => `${letter}`).join(' ')}

` : ''; + const consonantHTML = `

Consonants
${consonants.map(letter => `${letter}`).join(' ')}

`; + const vowelHTML = `

Vowels
${vowels.map(letter => `${letter}`).join(' ')}

`; + const blendHTML = blends.length > 0 ? `

Polyphthongs / Blends
${blends.map(letter => `${letter}`).join(' ')}

` : ''; const phonologyHTML = `

Phonology

${consonantHTML}
@@ -94,31 +94,35 @@ export function renderDetails() { ${blendHTML}`; const { onset, nucleus, coda } = phonotactics; - const onsetHTML = `

Onset: ${onset.map(letter => `${letter}`).join(' ')}

`; - const nucleusHTML = `

Nucleus: ${nucleus.map(letter => `${letter}`).join(' ')}

`; - const codaHTML = `

Coda: ${coda.map(letter => `${letter}`).join(' ')}

`; - const phonotacticsNotesHTML = phonotactics.notes.trim().length > 0 ? '

Notes:

' + md(removeTags(phonotactics.notes)) + '
' : ''; - const phonotacticsHTML = `

Phonotactics

+ const onsetHTML = `

Onset
${onset.map(letter => `${letter}`).join(' ')}

`; + const nucleusHTML = `

Nucleus
${nucleus.map(letter => `${letter}`).join(' ')}

`; + const codaHTML = `

Coda
${coda.map(letter => `${letter}`).join(' ')}

`; + const phonotacticsNotesHTML = phonotactics.notes.trim().length > 0 ? '

Notes

' + md(removeTags(phonotactics.notes)) + '
' : ''; + const phonotacticsHTML = onset.length + nucleus.length + coda.length + phonotacticsNotesHTML.length > 0 + ? `

Phonotactics

-
${onsetHTML}
-
${nucleusHTML}
-
${codaHTML}
+
${onsetHTML}
+
${nucleusHTML}
+
${codaHTML}
- ${phonotacticsNotesHTML}`; + ${phonotacticsNotesHTML}` + : ''; const { translations } = orthography; - const translationsHTML = `

Translations

${translations.map(translation => { + const translationsHTML = `

Translations
${translations.map(translation => { translation = translation.split('=').map(value => value.trim()); if (translation.length > 1 && translation[0] !== '' && translation[1] !== '') { return `${translation[0]}${translation[1]}`; } return false; - }).filter(html => html !== false).join(' ')}

`; - const orthographyNotesHTML = '

Notes

' + md(removeTags(orthography.notes)) + '
'; - const orthographyHTML = `

Orthography

+ }).filter(html => html !== false).join(' ')}

`; + const orthographyNotesHTML = orthography.notes.trim().length > 0 ? '

Notes
' + md(removeTags(orthography.notes)) + '

' : ''; + const orthographyHTML = translations.length > 0 && orthographyNotesHTML.length > 0 + ? `

Orthography

${translations.length > 0 ? translationsHTML : ''} - ${orthographyNotesHTML}`; - const grammarHTML = '

Grammar

Notes:

' + md(removeTags(grammar.notes)) + '
'; + ${orthographyNotesHTML}` + : ''; + const grammarHTML = '

Grammar

' + md(removeTags(grammar.notes)) + '
'; detailsPanel.innerHTML = generalHTML + phonologyHTML + phonotacticsHTML + orthographyHTML + grammarHTML; } diff --git a/src/js/view/render.js b/src/js/view/render.js index 9308782..5aeb88a 100644 --- a/src/js/view/render.js +++ b/src/js/view/render.js @@ -43,16 +43,16 @@ export function renderDescription() { export function renderDetails() { const { partsOfSpeech, alphabeticalOrder } = window.currentDictionary; const { phonology, phonotactics, orthography, grammar } = window.currentDictionary.details; - const partsOfSpeechHTML = `

Parts of Speech:

${partsOfSpeech.map(partOfSpeech => '' + partOfSpeech + '').join(' ')}
`; - const alphabeticalOrderHTML = `

Alphabetical Order:

${ + const partsOfSpeechHTML = `

Parts of Speech
${partsOfSpeech.map(partOfSpeech => '' + partOfSpeech + '').join(' ')}

`; + const alphabeticalOrderHTML = `

Alphabetical Order
${ (alphabeticalOrder.length > 0 ? alphabeticalOrder : ['English Alphabet']).map(letter => `${letter}`).join(' ') }`; const generalHTML = `

General

${partsOfSpeechHTML}${alphabeticalOrderHTML}`; const { consonants, vowels, blends } = phonology - const consonantHTML = `

Consonants:

${consonants.map(letter => `${letter}`).join(' ')}
`; - const vowelHTML = `

Vowels:

${vowels.map(letter => `${letter}`).join(' ')}
`; - const blendHTML = blends.length > 0 ? `

Polyphthongs / Blends:

${blends.map(letter => `${letter}`).join(' ')}

` : ''; + const consonantHTML = `

Consonants
${consonants.map(letter => `${letter}`).join(' ')}

`; + const vowelHTML = `

Vowels
${vowels.map(letter => `${letter}`).join(' ')}

`; + const blendHTML = blends.length > 0 ? `

Polyphthongs / Blends
${blends.map(letter => `${letter}`).join(' ')}

` : ''; const phonologyHTML = `

Phonology

${consonantHTML}
@@ -61,30 +61,34 @@ export function renderDetails() { ${blendHTML}`; const { onset, nucleus, coda } = phonotactics; - const onsetHTML = `

Onset:

${onset.map(letter => `${letter}`).join(' ')}
`; - const nucleusHTML = `

Nucleus:

${nucleus.map(letter => `${letter}`).join(' ')}
`; - const codaHTML = `

Coda:

${coda.map(letter => `${letter}`).join(' ')}
`; - const phonotacticsNotesHTML = phonotactics.notes.trim().length > 0 ? '

Notes:

' + md(removeTags(phonotactics.notes)) + '
' : ''; - const phonotacticsHTML = `

Phonotactics

+ const onsetHTML = `

Onset
${onset.map(letter => `${letter}`).join(' ')}

`; + const nucleusHTML = `

Nucleus
${nucleus.map(letter => `${letter}`).join(' ')}

`; + const codaHTML = `

Coda
${coda.map(letter => `${letter}`).join(' ')}

`; + const phonotacticsNotesHTML = phonotactics.notes.trim().length > 0 ? '

Notes

' + md(removeTags(phonotactics.notes)) + '
' : ''; + const phonotacticsHTML = onset.length + nucleus.length + coda.length + phonotacticsNotesHTML.length > 0 + ? `

Phonotactics

-
${onsetHTML}
-
${nucleusHTML}
-
${codaHTML}
+
${onsetHTML}
+
${nucleusHTML}
+
${codaHTML}
- ${phonotacticsNotesHTML}`; + ${phonotacticsNotesHTML}` + : ''; const { translations } = orthography; - const translationsHTML = `

Translations

${translations.map(translation => { + const translationsHTML = `

Translations
${translations.map(translation => { translation = translation.split('=').map(value => value.trim()); if (translation.length > 1 && translation[0] !== '' && translation[1] !== '') { - return `${translation[0]}${translation[1]}`; + return `${translation[0]}${translation[1]}`; } return false; - }).filter(html => html !== false).join(' ')}

`; - const orthographyNotesHTML = '

Notes

' + md(removeTags(orthography.notes)) + '
'; - const orthographyHTML = `

Orthography

+ }).filter(html => html !== false).join(' ')}

`; + const orthographyNotesHTML = orthography.notes.trim().length > 0 ? '

Notes
' + md(removeTags(orthography.notes)) + '

' : ''; + const orthographyHTML = translations.length > 0 && orthographyNotesHTML.length > 0 + ? `

Orthography

${translations.length > 0 ? translationsHTML : ''} - ${orthographyNotesHTML}`; + ${orthographyNotesHTML}` + : ''; const grammarHTML = '

Grammar

' + md(removeTags(grammar.notes)) + '
'; detailsPanel.innerHTML = generalHTML + phonologyHTML + phonotacticsHTML + orthographyHTML + grammarHTML; From f153e0c3eca1bee7d49bdb1c95d3ebc05e61440e Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Sat, 6 Jul 2019 23:00:09 -0600 Subject: [PATCH 105/134] Add extra classes to word references and translated text --- src/js/render.js | 4 ++-- src/js/view/render.js | 8 +++++--- src/js/view/wordManagement.js | 14 ++++++++++++-- src/js/wordManagement.js | 2 +- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/js/render.js b/src/js/render.js index e3a0e20..cab0567 100644 --- a/src/js/render.js +++ b/src/js/render.js @@ -112,7 +112,7 @@ export function renderDetails() { const translationsHTML = `

Translations
${translations.map(translation => { translation = translation.split('=').map(value => value.trim()); if (translation.length > 1 && translation[0] !== '' && translation[1] !== '') { - return `${translation[0]}${translation[1]}`; + return `${translation[0]}${translation[1]}`; } return false; }).filter(html => html !== false).join(' ')}

`; @@ -218,7 +218,7 @@ export function renderWords() { wordsHTML += `
-

${wordNameDisplay}${homonymnNumber > 0 ? ' ' + homonymnNumber.toString() + '' : ''}

+

${wordNameDisplay}${homonymnNumber > 0 ? ' ' + homonymnNumber.toString() + '' : ''}

${word.pronunciation} ${word.partOfSpeech} ${isPublic ? `` : ''} diff --git a/src/js/view/render.js b/src/js/view/render.js index 5aeb88a..da0fc5f 100644 --- a/src/js/view/render.js +++ b/src/js/view/render.js @@ -4,7 +4,7 @@ import { getWordsStats, getHomonymnNumber } from './utilities'; import { getMatchingSearchWords, highlightSearchTerm, getSearchFilters, getSearchTerm } from './search'; import { showSection } from './displayToggles'; import { setupSearchFilters, setupInfoModal } from './setupListeners'; -import { parseReferences } from './wordManagement'; +import { parseReferences, translateOrthography } from './wordManagement'; import { renderAd } from '../ads'; import { sortWords } from './wordManagement'; @@ -79,7 +79,7 @@ export function renderDetails() { const translationsHTML = `

Translations
${translations.map(translation => { translation = translation.split('=').map(value => value.trim()); if (translation.length > 1 && translation[0] !== '' && translation[1] !== '') { - return `${translation[0]}${translation[1]}`; + return `${translation[0]}${translation[1]}`; } return false; }).filter(html => html !== false).join(' ')}

`; @@ -170,9 +170,11 @@ export function renderWords() { wordsHTML += renderAd(displayIndex); + let wordNameDisplay = translateOrthography(word.name); + wordsHTML += `
-

${word.name}${homonymnNumber > 0 ? ' ' + homonymnNumber.toString() + '' : ''}

+

${wordNameDisplay}${homonymnNumber > 0 ? ' ' + homonymnNumber.toString() + '' : ''}

${word.pronunciation} ${word.partOfSpeech} diff --git a/src/js/view/wordManagement.js b/src/js/view/wordManagement.js index 8dfb04f..6a61ff0 100644 --- a/src/js/view/wordManagement.js +++ b/src/js/view/wordManagement.js @@ -11,13 +11,23 @@ export function sortWords() { }); } +export function translateOrthography(word) { + window.currentDictionary.details.orthography.translations.forEach(translation => { + translation = translation.split('=').map(value => value.trim()); + if (translation.length > 1 && translation[0] !== '' && translation[1] !== '') { + word = word.replace(new RegExp(translation[0], 'g'), translation[1]); + } + }); + return word; +} + 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); @@ -46,7 +56,7 @@ export function parseReferences(detailsMarkdown) { homonymn = 1; } const homonymnSubHTML = homonymn > 0 ? '' + homonymn.toString() + '' : ''; - const wordMarkdownLink = `[${wordToFind}${homonymnSubHTML}](#${existingWordId})`; + const wordMarkdownLink = `[${translateOrthography(wordToFind)}${homonymnSubHTML}](#${existingWordId})`; detailsMarkdown = detailsMarkdown.replace(new RegExp(reference, 'g'), wordMarkdownLink); } }); diff --git a/src/js/wordManagement.js b/src/js/wordManagement.js index a192346..e037beb 100644 --- a/src/js/wordManagement.js +++ b/src/js/wordManagement.js @@ -91,7 +91,7 @@ export function parseReferences(detailsMarkdown) { homonymn = 1; } const homonymnSubHTML = homonymn > 0 ? '' + homonymn.toString() + '' : ''; - const wordMarkdownLink = `[${translateOrthography(wordToFind)}${homonymnSubHTML}](#${existingWordId})`; + const wordMarkdownLink = `[${translateOrthography(wordToFind)}${homonymnSubHTML}](#${existingWordId})`; detailsMarkdown = detailsMarkdown.replace(new RegExp(reference, 'g'), wordMarkdownLink); } }); From d36eec52fab4b59ffd3d397f598bf13ee40dc601 Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Sat, 6 Jul 2019 23:01:17 -0600 Subject: [PATCH 106/134] Replace Ctrl+X hotkey with Ctrl+Backspace/Delete --- src/js/hotkeys.js | 3 ++- src/markdown/help.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/js/hotkeys.js b/src/js/hotkeys.js index 0aeca68..f186926 100644 --- a/src/js/hotkeys.js +++ b/src/js/hotkeys.js @@ -56,7 +56,8 @@ export function hotKeyActions(event) { break; } case 'S': if (event.ctrlKey) {event.preventDefault(); hideAllModals(); openSettingsModal();} break; - case 'x': if (event.ctrlKey) {event.preventDefault(); clearSearchText();} break; + case 'Delete': + case 'Backspace': if (event.ctrlKey) {event.preventDefault(); clearSearchText();} break; } } diff --git a/src/markdown/help.md b/src/markdown/help.md index 58f91ea..f609e41 100644 --- a/src/markdown/help.md +++ b/src/markdown/help.md @@ -170,7 +170,7 @@ After making any changes, be sure to click "Save" or "Save & Close" to ensure th - **M:** Maximize/Minimize Full Screen textbox when typing in the boxes that have the Maximize button. - **S:** Open the Search panel. - **Shift + S:** Open the Settings window. -- **X:** Clear the Search box. +- **Backspace/Delete:** Clear the Search box. ## Accounts If you are using an account with Lexiconga, your experience should remain essentially the same, but you will see some additional options in the Settings menu and you might notice some slight changes in performance as it saves to and loads from the database. This saving/loading process prioritizes your local dictionary, so if you ever lose connection, it will keep retrying the upload until connection is re-established. It also attempts to sync every time you load Lexiconga, so please be aware of that if you refresh the page. From 850b042d6b200e3bc822fb08bc89c04668077fe9 Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Sun, 7 Jul 2019 00:00:32 -0600 Subject: [PATCH 107/134] Do reference parsing and orthography translation on backend for public view --- src/js/view/render.js | 9 ++---- src/js/view/wordManagement.js | 54 ----------------------------------- src/php/api/Dictionary.php | 39 ++++++++++++++++++++----- 3 files changed, 35 insertions(+), 67 deletions(-) diff --git a/src/js/view/render.js b/src/js/view/render.js index da0fc5f..8ed7090 100644 --- a/src/js/view/render.js +++ b/src/js/view/render.js @@ -4,7 +4,6 @@ import { getWordsStats, getHomonymnNumber } from './utilities'; import { getMatchingSearchWords, highlightSearchTerm, getSearchFilters, getSearchTerm } from './search'; import { showSection } from './displayToggles'; import { setupSearchFilters, setupInfoModal } from './setupListeners'; -import { parseReferences, translateOrthography } from './wordManagement'; import { renderAd } from '../ads'; import { sortWords } from './wordManagement'; @@ -35,7 +34,7 @@ export function renderName() { } export function renderDescription() { - const descriptionHTML = md(removeTags(window.currentDictionary.description)); + const descriptionHTML = md(window.currentDictionary.description); document.getElementById('detailsPanel').innerHTML = '
' + descriptionHTML + '
'; } @@ -161,7 +160,7 @@ export function renderWords() { pronunciation: removeTags(originalWord.pronunciation), partOfSpeech: removeTags(originalWord.partOfSpeech), definition: removeTags(originalWord.definition), - details: parseReferences(removeTags(originalWord.details)), + details: originalWord.details, wordId: originalWord.wordId, }); @@ -170,11 +169,9 @@ export function renderWords() { wordsHTML += renderAd(displayIndex); - let wordNameDisplay = translateOrthography(word.name); - wordsHTML += `
-

${wordNameDisplay}${homonymnNumber > 0 ? ' ' + homonymnNumber.toString() + '' : ''}

+

${word.name}${homonymnNumber > 0 ? ' ' + homonymnNumber.toString() + '' : ''}

${word.pronunciation} ${word.partOfSpeech} diff --git a/src/js/view/wordManagement.js b/src/js/view/wordManagement.js index 6a61ff0..27de19a 100644 --- a/src/js/view/wordManagement.js +++ b/src/js/view/wordManagement.js @@ -1,4 +1,3 @@ -import { wordExists, getHomonymnIndexes } from "./utilities"; import removeDiacritics from "../StackOverflow/removeDiacritics"; export function sortWords() { @@ -10,56 +9,3 @@ export function sortWords() { return removeDiacritics(wordA[sortBy]).toLowerCase() > removeDiacritics(wordB[sortBy]).toLowerCase() ? 1 : -1; }); } - -export function translateOrthography(word) { - window.currentDictionary.details.orthography.translations.forEach(translation => { - translation = translation.split('=').map(value => value.trim()); - if (translation.length > 1 && translation[0] !== '' && translation[1] !== '') { - word = word.replace(new RegExp(translation[0], 'g'), translation[1]); - } - }); - return word; -} - -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 ? '' + homonymn.toString() + '' : ''; - const wordMarkdownLink = `[${translateOrthography(wordToFind)}${homonymnSubHTML}](#${existingWordId})`; - detailsMarkdown = detailsMarkdown.replace(new RegExp(reference, 'g'), wordMarkdownLink); - } - }); - } - return detailsMarkdown; -} diff --git a/src/php/api/Dictionary.php b/src/php/api/Dictionary.php index 0e9c224..21fb3d4 100644 --- a/src/php/api/Dictionary.php +++ b/src/php/api/Dictionary.php @@ -101,7 +101,7 @@ VALUES ($new_id, ?, ?, ?, ?, ?)"; 'externalID' => $result['id'], 'name' => $result['name'], 'specification' => $result['specification'], - 'description' => $result['description'], + 'description' => $this->parseReferences(strip_tags($result['description']), $result['id']), 'createdBy' => $result['public_name'], 'partsOfSpeech' => explode(',', $partsOfSpeech), 'alphabeticalOrder' => array(), @@ -147,11 +147,11 @@ VALUES ($new_id, ?, ?, ?, ?, ?)"; if ($results) { return array_map(function ($row) use ($dictionary) { return array( - 'name' => $row['name'], + 'name' => $this->translateOrthography($row['name'], $dictionary), 'pronunciation' => $row['pronunciation'], 'partOfSpeech' => $row['part_of_speech'], 'definition' => $row['definition'], - 'details' => $this->parseReferences($row['details'], $dictionary), + 'details' => $this->parseReferences(strip_tags($row['details']), $dictionary), 'lastUpdated' => is_null($row['last_updated']) ? intval($row['created_on']) : intval($row['last_updated']), 'createdOn' => intval($row['created_on']), 'wordId' => intval($row['word_id']), @@ -168,11 +168,11 @@ VALUES ($new_id, ?, ?, ?, ?, ?)"; $result = $this->db->query($query, array($dictionary, $word))->fetch(); if ($result) { return array( - 'name' => $result['name'], + 'name' => $this->translateOrthography($result['name'], $dictionary), 'pronunciation' => $result['pronunciation'], 'partOfSpeech' => $result['part_of_speech'], 'definition' => $result['definition'], - 'details' => $this->parseReferences($result['details'], $dictionary), + 'details' => $this->parseReferences(strip_tags($result['details']), $dictionary), 'lastUpdated' => is_null($result['last_updated']) ? intval($result['created_on']) : intval($result['last_updated']), 'createdOn' => intval($result['created_on']), 'wordId' => intval($result['word_id']), @@ -219,8 +219,9 @@ VALUES ($new_id, ?, ?, ?, ?, ?)"; } $homonymn_sub_html = $homonymn > 0 ? '' . $homonymn . '' : ''; $site_root = substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], $dictionary_id)); - $markdown_link = '' - . $word_to_find . $homonymn_sub_html . ''; + $markdown_link = '' + . '' . $this->translateOrthography($word_to_find, $dictionary_id) . '' . $homonymn_sub_html + . ''; $details = str_replace($reference, $markdown_link, $details); } } @@ -243,6 +244,30 @@ VALUES ($new_id, ?, ?, ?, ?, ?)"; return array(); } + private function translateOrthography($word, $dictionary) { + if (!isset($this->translations)) { + $this->translations = $this->getTranslations($dictionary); + } + foreach($this->translations as $translation) { + $translation = array_map('trim', explode('=', $translation)); + if (count($translation) > 1 && $translation[0] !== '' && $translation[1] !== '') { + $word = str_replace($translation[0], $translation[1], $word); + } + }; + return $word; + } + + private function getTranslations($dictionary) { + if (is_numeric($dictionary)) { + $query = "SELECT translations FROM dictionary_linguistics WHERE dictionary=?"; + $result = $this->db->query($query, array($dictionary))->fetch(); + if ($result) { + return explode(PHP_EOL, $result['translations']); + } + } + return array(); + } + public function getDetails ($user, $dictionary) { $query = "SELECT * FROM dictionaries JOIN dictionary_linguistics ON dictionary = id WHERE user=$user AND id=$dictionary"; $result = $this->db->query($query)->fetch(); From 079288a0a8a4c1767b0c0643488095948fa145d5 Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Sun, 7 Jul 2019 00:00:53 -0600 Subject: [PATCH 108/134] Add translations to structure.sql --- src/structure.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/src/structure.sql b/src/structure.sql index 3511548..8deeea9 100644 --- a/src/structure.sql +++ b/src/structure.sql @@ -41,6 +41,7 @@ CREATE TABLE IF NOT EXISTS `dictionary_linguistics` ( `nucleus` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'Comma-separated', `coda` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'Comma-separated', `phonotactics_notes` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT 'Markdown', + `translations` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT 'Newline-separated; Translates left character(s) to right character(s)', `orthography_notes` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT 'Markdown', `grammar_notes` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT 'Markdown', UNIQUE KEY `dictionary` (`dictionary`) From 45e9e5230ce328fd474af2d12fd96f8061510c8f Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Sun, 7 Jul 2019 00:09:55 -0600 Subject: [PATCH 109/134] Correctly show/hide homonymn number --- src/js/wordManagement.js | 2 +- src/php/api/Dictionary.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/wordManagement.js b/src/js/wordManagement.js index e037beb..fe95e2c 100644 --- a/src/js/wordManagement.js +++ b/src/js/wordManagement.js @@ -90,7 +90,7 @@ export function parseReferences(detailsMarkdown) { if (homonymn < 1 && homonymnIndexes.length > 0) { homonymn = 1; } - const homonymnSubHTML = homonymn > 0 ? '' + homonymn.toString() + '' : ''; + const homonymnSubHTML = homonymnIndexes.length > 1 && homonymn - 1 >= 0 ? '' + homonymn.toString() + '' : ''; const wordMarkdownLink = `[${translateOrthography(wordToFind)}${homonymnSubHTML}](#${existingWordId})`; detailsMarkdown = detailsMarkdown.replace(new RegExp(reference, 'g'), wordMarkdownLink); } diff --git a/src/php/api/Dictionary.php b/src/php/api/Dictionary.php index 21fb3d4..fc7412a 100644 --- a/src/php/api/Dictionary.php +++ b/src/php/api/Dictionary.php @@ -217,7 +217,7 @@ VALUES ($new_id, ?, ?, ?, ?, ?)"; if ($homonymn < 1) { $homonymn = 1; } - $homonymn_sub_html = $homonymn > 0 ? '' . $homonymn . '' : ''; + $homonymn_sub_html = count($reference_ids) > 1 && $homonymn - 1 >= 0 ? '' . $homonymn . '' : ''; $site_root = substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], $dictionary_id)); $markdown_link = '' . '' . $this->translateOrthography($word_to_find, $dictionary_id) . '' . $homonymn_sub_html From 460506012ebdcf074b9bac783490563859a11411 Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Sun, 7 Jul 2019 11:32:05 -0600 Subject: [PATCH 110/134] Start setting up custom alphabetical order Need to figure out why non-alphabetical letters are sorting wrong. They should be at the end no matter what, but they're not always. --- src/js/wordManagement.js | 48 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/js/wordManagement.js b/src/js/wordManagement.js index fe95e2c..a4131e6 100644 --- a/src/js/wordManagement.js +++ b/src/js/wordManagement.js @@ -32,9 +32,57 @@ export function validateWord(word, wordId = false) { export function sortWords(render) { const { sortByDefinition } = window.currentDictionary.settings; + const { alphabeticalOrder } = window.currentDictionary; const sortBy = sortByDefinition ? 'definition' : 'name'; + const ordering = {}; // map for efficient lookup of sortIndex + for (let i = 0; i < alphabeticalOrder.length; i++) { + ordering[alphabeticalOrder[i]] = i; + } + window.currentDictionary.words.sort((wordA, wordB) => { + if (wordA[sortBy] === wordB[sortBy]) return 0; + + const aLetters = wordA[sortBy].split(''); + const bLetters = wordB[sortBy].split(''); + + for (let i = 0; i < aLetters.length; i++) { + const a = aLetters[i]; + if (ordering.hasOwnProperty(a)) { // if a is in the alphabet... + if (typeof bLetters[i] !== 'undefined') { // and if wordB has a letter at the same position... + const b = bLetters[i]; + if (ordering.hasOwnProperty(b)) { // and b is in the alphabet then compare the letters + const aIndex = ordering[a]; + const bIndex = ordering[b]; + if (aIndex === bIndex) { // If the letters are the same, then... + // If wordA is shorter than wordB and this is wordA's last letter, then sort a first; + if (aLetters.length < bLetters.length && i == aLetters.length - 1) return -1; + continue; // Otherwise if it is the same letter, check the next letter + } + return aIndex - bIndex; // If different and both in alphabet, compare alphabetical order + } + } else { + return 1; // If b is shorter than a after looping, then sort a after + } + } else if (ordering.hasOwnProperty(bLetters[i])) { + return 1; // If a is not in the alphabet but b is, sort a after + } else { + if (typeof bLetters[i] !== 'undefined') { // and if wordB has a letter at the same position... + const b = bLetters[i]; + if (removeDiacritics(a).toLowerCase() === removeDiacritics(b).toLowerCase()) { + if (aLetters.length < bLetters.length && i == aLetters.length - 1) return -1; + continue; + } + return removeDiacritics(a).toLowerCase() > removeDiacritics(b).toLowerCase() ? 1 : -1; + } else { + return 1; // If b is shorter than a after looping, then sort a after + } + } + } + + console.log('done looping, comparing normally:', wordA[sortBy], wordB[sortBy]); + + // If after looping, the alphabet is still different, just compare the words alphabetically. if (removeDiacritics(wordA[sortBy]).toLowerCase() === removeDiacritics(wordB[sortBy]).toLowerCase()) return 0; return removeDiacritics(wordA[sortBy]).toLowerCase() > removeDiacritics(wordB[sortBy]).toLowerCase() ? 1 : -1; }); From ee30fe53b289a2d470e7ad228c8281296138919c Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Mon, 8 Jul 2019 15:49:46 -0600 Subject: [PATCH 111/134] Split render.js out into separate files --- src/js/dictionaryManagement.js | 3 +- src/js/displayToggles.js | 2 +- src/js/hotkeys.js | 2 +- src/js/pagination.js | 2 +- src/js/render.js | 386 --------------------------------- src/js/render/details.js | 138 ++++++++++++ src/js/render/index.js | 14 ++ src/js/render/modals.js | 74 +++++++ src/js/render/words.js | 172 +++++++++++++++ src/js/search.js | 2 +- src/js/settings.js | 2 +- src/js/setupListeners.js | 3 +- src/js/wordManagement.js | 2 +- 13 files changed, 408 insertions(+), 394 deletions(-) delete mode 100644 src/js/render.js create mode 100644 src/js/render/details.js create mode 100644 src/js/render/index.js create mode 100644 src/js/render/modals.js create mode 100644 src/js/render/words.js diff --git a/src/js/dictionaryManagement.js b/src/js/dictionaryManagement.js index c464d7e..176fc31 100644 --- a/src/js/dictionaryManagement.js +++ b/src/js/dictionaryManagement.js @@ -1,5 +1,6 @@ import papa from 'papaparse'; -import { renderDictionaryDetails, renderPartsOfSpeech, renderAll, renderTheme } from "./render"; +import { renderDictionaryDetails, renderPartsOfSpeech } from "./render/details"; +import { renderAll, renderTheme } from "./render"; import { removeTags, cloneObject, getTimestampInSeconds, download, slugify } from "../helpers"; import { LOCAL_STORAGE_KEY, DEFAULT_DICTIONARY } from "../constants"; import { addMessage, getNextId, hasToken } from "./utilities"; diff --git a/src/js/displayToggles.js b/src/js/displayToggles.js index bf1d893..2f7081c 100644 --- a/src/js/displayToggles.js +++ b/src/js/displayToggles.js @@ -1,4 +1,4 @@ -import { renderDescription, renderDetails, renderStats } from './render'; +import { renderDescription, renderDetails, renderStats } from './render/details'; export function showSection(sectionName) { switch (sectionName) { diff --git a/src/js/hotkeys.js b/src/js/hotkeys.js index f186926..bc74c54 100644 --- a/src/js/hotkeys.js +++ b/src/js/hotkeys.js @@ -1,6 +1,6 @@ import { confirmEditWord, submitWordForm } from "./wordManagement"; import { showSection, hideDetailsPanel } from "./displayToggles"; -import { renderInfoModal, renderMaximizedTextbox } from "./render"; +import { renderInfoModal, renderMaximizedTextbox } from "./render/modals"; import { showSearchModal, clearSearchText } from "./search"; import { saveAndCloseSettingsModal, openSettingsModal, saveSettings } from "./settings"; import { saveAndCloseEditModal, openEditModal } from "./dictionaryManagement"; diff --git a/src/js/pagination.js b/src/js/pagination.js index 46e6083..553f906 100644 --- a/src/js/pagination.js +++ b/src/js/pagination.js @@ -1,5 +1,5 @@ import { DEFAULT_PAGE_SIZE } from '../constants'; -import { renderWords } from "./render"; +import { renderWords } from "./render/words"; export function getPaginationData(words) { const numWords = words.length; diff --git a/src/js/render.js b/src/js/render.js deleted file mode 100644 index cab0567..0000000 --- a/src/js/render.js +++ /dev/null @@ -1,386 +0,0 @@ -import md from 'marked'; -import { removeTags, slugify } from '../helpers'; -import { getWordsStats, getHomonymnNumber, hasToken } from './utilities'; -import { getMatchingSearchWords, highlightSearchTerm, getSearchFilters, getSearchTerm } from './search'; -import { showSection } from './displayToggles'; -import { - setupSearchFilters, - setupWordOptionButtons, - setupPagination, - setupWordOptionSelections, - setupWordEditFormButtons, - setupMaximizeModal, - setupInfoModal, - setupIPATable, - setupIPAFields -} from './setupListeners'; -import { getPaginationData } from './pagination'; -import { getOpenEditForms, translateOrthography, parseReferences } from './wordManagement'; -import { renderAd } from './ads'; -import ipaTableFile from './KeyboardFire/phondue/ipa-table.html'; -import { getPublicLink } from './account/utilities'; - -export function renderAll() { - renderTheme(); - renderDictionaryDetails(); - renderPartsOfSpeech(); - renderWords(); -} - -export function renderTheme() { - const { theme } = window.currentDictionary.settings; - document.body.id = theme + 'Theme'; -} - -export function renderDictionaryDetails() { - renderName(); - - const tabs = document.querySelectorAll('#detailsSection nav li'); - const shownTab = Array.from(tabs).find(tab => tab.classList.contains('active')); - if (shownTab) { - const tabName = shownTab.innerText.toLowerCase(); - showSection(tabName); - } -} - -export function renderName() { - const dictionaryName = removeTags(window.currentDictionary.name) + ' ' + removeTags(window.currentDictionary.specification); - const name = document.getElementById('dictionaryName'); - name.innerHTML = dictionaryName; - const isPublic = hasToken() && window.currentDictionary.settings.isPublic; - const shareLinkElement = document.getElementById('dictionaryShare'); - - if (isPublic && !shareLinkElement) { - const shareLink = document.createElement('a'); - shareLink.id = 'dictionaryShare'; - shareLink.classList.add('button'); - shareLink.style.float = 'right'; - shareLink.href = getPublicLink(); - shareLink.target = '_blank'; - shareLink.title = 'Public Link to Dictionary'; - shareLink.innerHTML = '➦'; - name.parentElement.insertBefore(shareLink, name); - } else if (isPublic && shareLinkElement) { - shareLinkElement.href = getPublicLink(); - } else if (!isPublic && shareLinkElement) { - shareLinkElement.parentElement.removeChild(shareLinkElement); - } -} - -export function renderDescription() { - const descriptionHTML = md(parseReferences(removeTags(window.currentDictionary.description))); - - document.getElementById('detailsPanel').innerHTML = '
' + descriptionHTML + '
'; -} - -export function renderDetails() { - const { partsOfSpeech, alphabeticalOrder } = window.currentDictionary; - const { phonology, phonotactics, orthography, grammar } = window.currentDictionary.details; - const partsOfSpeechHTML = `

Parts of Speech
${partsOfSpeech.map(partOfSpeech => '' + partOfSpeech + '').join(' ')}

`; - const alphabeticalOrderHTML = `

Alphabetical Order
${ - (alphabeticalOrder.length > 0 ? alphabeticalOrder : ['English Alphabet']).map(letter => `${letter}`).join(' ') - }`; - const generalHTML = `

General

${partsOfSpeechHTML}${alphabeticalOrderHTML}`; - - const { consonants, vowels, blends } = phonology - const consonantHTML = `

Consonants
${consonants.map(letter => `${letter}`).join(' ')}

`; - const vowelHTML = `

Vowels
${vowels.map(letter => `${letter}`).join(' ')}

`; - const blendHTML = blends.length > 0 ? `

Polyphthongs / Blends
${blends.map(letter => `${letter}`).join(' ')}

` : ''; - const phonologyHTML = `

Phonology

-
-
${consonantHTML}
-
${vowelHTML}
-
- ${blendHTML}`; - - const { onset, nucleus, coda } = phonotactics; - const onsetHTML = `

Onset
${onset.map(letter => `${letter}`).join(' ')}

`; - const nucleusHTML = `

Nucleus
${nucleus.map(letter => `${letter}`).join(' ')}

`; - const codaHTML = `

Coda
${coda.map(letter => `${letter}`).join(' ')}

`; - const phonotacticsNotesHTML = phonotactics.notes.trim().length > 0 ? '

Notes

' + md(removeTags(phonotactics.notes)) + '
' : ''; - const phonotacticsHTML = onset.length + nucleus.length + coda.length + phonotacticsNotesHTML.length > 0 - ? `

Phonotactics

-
-
${onsetHTML}
-
${nucleusHTML}
-
${codaHTML}
-
- ${phonotacticsNotesHTML}` - : ''; - - const { translations } = orthography; - const translationsHTML = `

Translations
${translations.map(translation => { - translation = translation.split('=').map(value => value.trim()); - if (translation.length > 1 && translation[0] !== '' && translation[1] !== '') { - return `${translation[0]}${translation[1]}`; - } - return false; - }).filter(html => html !== false).join(' ')}

`; - const orthographyNotesHTML = orthography.notes.trim().length > 0 ? '

Notes
' + md(removeTags(orthography.notes)) + '' : ''; - const orthographyHTML = translations.length > 0 && orthographyNotesHTML.length > 0 - ? `

Orthography

- ${translations.length > 0 ? translationsHTML : ''} - ${orthographyNotesHTML}` - : ''; - const grammarHTML = '

Grammar

' + md(removeTags(grammar.notes)) + '
'; - - detailsPanel.innerHTML = generalHTML + phonologyHTML + phonotacticsHTML + orthographyHTML + grammarHTML; -} - -export function renderStats() { - const wordStats = getWordsStats(); - const numberOfWordsHTML = `

Number of Words
${wordStats.numberOfWords.map(stat => `${stat.name}${stat.value}`).join(' ')}

`; - const wordLengthHTML = `

Word Length
Shortest${wordStats.wordLength.shortest} - Longest${wordStats.wordLength.longest} - Average${wordStats.wordLength.average}

`; - const letterDistributionHTML = `

Letter Distribution
${wordStats.letterDistribution.map(stat => `${stat.letter}${stat.percentage.toFixed(2)}`).join(' ')}

`; - const totalLettersHTML = `

${wordStats.totalLetters} Total Letters

`; - - detailsPanel.innerHTML = numberOfWordsHTML + wordLengthHTML + letterDistributionHTML + totalLettersHTML; -} - -export function renderPartsOfSpeech(onlyOptions = false) { - let optionsHTML = '', - searchHTML = ''; - window.currentDictionary.partsOfSpeech.forEach(partOfSpeech => { - partOfSpeech = removeTags(partOfSpeech); - optionsHTML += ``; - searchHTML += ``; - }); - searchHTML += `
Check All Uncheck All`; - - Array.from(document.getElementsByClassName('part-of-speech-select')).forEach(select => { - const selectedValue = select.value; - select.innerHTML = optionsHTML; - select.value = selectedValue; - }); - if (!onlyOptions) { - document.getElementById('searchPartsOfSpeech').innerHTML = searchHTML; - } - - setupSearchFilters(); -} - -export function renderWords() { - let wordsHTML = ''; - let openEditForms = getOpenEditForms(); - let words = false; - const isPublic = hasToken() && window.currentDictionary.settings.isPublic; - - if (window.currentDictionary.words.length === 0) { - wordsHTML = `
-
-

No Words Created

-
-
-
Use the Word Form to create words or click the Help button below!
-
-
`; - } else { - words = getMatchingSearchWords(); - - if (words.length === 0) { - wordsHTML = `
-
-

No Search Results

-
-
-
Edit your search or filter to show words.
-
-
`; - } - - if (openEditForms.length > 0) { - // Clone the dom nodes - openEditForms.forEach((wordFormId, index) => { - openEditForms[index] = document.getElementById(wordFormId.toString()).cloneNode(true); - }); - } - - // const { pageStart, pageEnd } = getPaginationData(words); - - // words.slice(pageStart, pageEnd).forEach(originalWord => { - words.forEach((originalWord, displayIndex) => { - const word = highlightSearchTerm({ - name: removeTags(originalWord.name), - pronunciation: removeTags(originalWord.pronunciation), - partOfSpeech: removeTags(originalWord.partOfSpeech), - definition: removeTags(originalWord.definition), - details: parseReferences(removeTags(originalWord.details)), - wordId: originalWord.wordId, - }); - const homonymnNumber = getHomonymnNumber(originalWord); - const shareLink = window.currentDictionary.hasOwnProperty('externalID') ? getPublicLink() + '/' + word.wordId : ''; - - wordsHTML += renderAd(displayIndex); - - let wordNameDisplay = translateOrthography(word.name); - - wordsHTML += `
-
-

${wordNameDisplay}${homonymnNumber > 0 ? ' ' + homonymnNumber.toString() + '' : ''}

- ${word.pronunciation} - ${word.partOfSpeech} - ${isPublic ? `` : ''} - Options - -
-
-
${word.definition}
-
- ${md(word.details)} -
-
-
`; - }); - } - - document.getElementById('entries').innerHTML = wordsHTML; - - if (openEditForms.length > 0) { - // Clone the dom nodes - openEditForms.forEach(editForm => { - const entryElement = document.getElementById(editForm.id); - entryElement.parentNode.replaceChild(editForm, entryElement); - }); - setupWordEditFormButtons(); - } - - setupWordOptionButtons(); - setupWordOptionSelections(); - - // Show Search Results - const searchTerm = getSearchTerm(); - const filters = getSearchFilters(); - let resultsText = searchTerm !== '' || !filters.allPartsOfSpeechChecked ? (words ? words.length : 0).toString() + ' Results' : ''; - resultsText += !filters.allPartsOfSpeechChecked ? ' (Filtered)' : ''; - document.getElementById('searchResults').innerHTML = resultsText; - - // renderPagination(words); -} - -export function renderPagination(filteredWords) { - const paginationData = getPaginationData(filteredWords); - - if (paginationData.pages > 0) { - let paginationHTML = (paginationData.currentPage > 0 ? '« Previous' : '') - + '' - + (paginationData.currentPage < paginationData.pages - 1 ? 'Next »' : ''); - - Array.from(document.getElementsByClassName('pagination')).forEach(pagination => { - pagination.innerHTML = paginationHTML; - }); - - setupPagination(); - } -} - -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 = `
- Field Help`; - const plainPronunciationField = ``; - const editForm = ` - - - - - -
- Save Changes - Cancel Edit - `; - - document.getElementById(wordId.toString()).innerHTML = editForm; - setupWordEditFormButtons(); - renderPartsOfSpeech(true); - } -} - -export function renderIPAHelp() { - import('./KeyboardFire/phondue/usage.html').then(html => { - renderInfoModal(html); - }); -} - -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 = ` - `; - - document.body.appendChild(modalElement); - - setupIPAFields(); - setupIPATable(modalElement, textBox); -} - -export function renderMaximizedTextbox(maximizeButton) { - maximizeButton = typeof maximizeButton.target === 'undefined' || maximizeButton.target === '' ? maximizeButton : maximizeButton.target; - const label = maximizeButton.parentElement.innerText.replace(/(\*|Maximize)/g, '').trim(); - const textBox = maximizeButton.parentElement.querySelector('textarea'); - const modalElement = document.createElement('section'); - modalElement.classList.add('modal', 'maximize-modal'); - modalElement.innerHTML = ` - `; - - document.body.appendChild(modalElement); - - setupMaximizeModal(modalElement, textBox); -} - -export function renderInfoModal(content) { - const modalElement = document.createElement('section'); - modalElement.classList.add('modal', 'info-modal'); - modalElement.innerHTML = ` - `; - - document.body.appendChild(modalElement); - - setupInfoModal(modalElement); -} diff --git a/src/js/render/details.js b/src/js/render/details.js new file mode 100644 index 0000000..64c33ab --- /dev/null +++ b/src/js/render/details.js @@ -0,0 +1,138 @@ +import md from 'marked'; +import { removeTags, slugify } from '../../helpers'; +import { getWordsStats, hasToken } from '../utilities'; +import { showSection } from '../displayToggles'; +import { + setupSearchFilters, +} from '../setupListeners'; +import { parseReferences } from '../wordManagement'; +import { getPublicLink } from '../account/utilities'; + +export function renderDictionaryDetails() { + renderName(); + + const tabs = document.querySelectorAll('#detailsSection nav li'); + const shownTab = Array.from(tabs).find(tab => tab.classList.contains('active')); + if (shownTab) { + const tabName = shownTab.innerText.toLowerCase(); + showSection(tabName); + } +} + +export function renderName() { + const dictionaryName = removeTags(window.currentDictionary.name) + ' ' + removeTags(window.currentDictionary.specification); + const name = document.getElementById('dictionaryName'); + name.innerHTML = dictionaryName; + const isPublic = hasToken() && window.currentDictionary.settings.isPublic; + const shareLinkElement = document.getElementById('dictionaryShare'); + + if (isPublic && !shareLinkElement) { + const shareLink = document.createElement('a'); + shareLink.id = 'dictionaryShare'; + shareLink.classList.add('button'); + shareLink.style.float = 'right'; + shareLink.href = getPublicLink(); + shareLink.target = '_blank'; + shareLink.title = 'Public Link to Dictionary'; + shareLink.innerHTML = '➦'; + name.parentElement.insertBefore(shareLink, name); + } else if (isPublic && shareLinkElement) { + shareLinkElement.href = getPublicLink(); + } else if (!isPublic && shareLinkElement) { + shareLinkElement.parentElement.removeChild(shareLinkElement); + } +} + +export function renderDescription() { + const descriptionHTML = md(parseReferences(removeTags(window.currentDictionary.description))); + + document.getElementById('detailsPanel').innerHTML = '
' + descriptionHTML + '
'; +} + +export function renderDetails() { + const { partsOfSpeech, alphabeticalOrder } = window.currentDictionary; + const { phonology, phonotactics, orthography, grammar } = window.currentDictionary.details; + const partsOfSpeechHTML = `

Parts of Speech
${partsOfSpeech.map(partOfSpeech => '' + partOfSpeech + '').join(' ')}`; + const alphabeticalOrderHTML = `

Alphabetical Order
${ + (alphabeticalOrder.length > 0 ? alphabeticalOrder : ['English Alphabet']).map(letter => `${letter}`).join(' ') + }`; + const generalHTML = `

General

${partsOfSpeechHTML}${alphabeticalOrderHTML}`; + + const { consonants, vowels, blends } = phonology + const consonantHTML = `

Consonants
${consonants.map(letter => `${letter}`).join(' ')}

`; + const vowelHTML = `

Vowels
${vowels.map(letter => `${letter}`).join(' ')}

`; + const blendHTML = blends.length > 0 ? `

Polyphthongs / Blends
${blends.map(letter => `${letter}`).join(' ')}

` : ''; + const phonologyHTML = `

Phonology

+
+
${consonantHTML}
+
${vowelHTML}
+
+ ${blendHTML}`; + + const { onset, nucleus, coda } = phonotactics; + const onsetHTML = `

Onset
${onset.map(letter => `${letter}`).join(' ')}

`; + const nucleusHTML = `

Nucleus
${nucleus.map(letter => `${letter}`).join(' ')}

`; + const codaHTML = `

Coda
${coda.map(letter => `${letter}`).join(' ')}

`; + const phonotacticsNotesHTML = phonotactics.notes.trim().length > 0 ? '

Notes

' + md(removeTags(phonotactics.notes)) + '
' : ''; + const phonotacticsHTML = onset.length + nucleus.length + coda.length + phonotacticsNotesHTML.length > 0 + ? `

Phonotactics

+
+
${onsetHTML}
+
${nucleusHTML}
+
${codaHTML}
+
+ ${phonotacticsNotesHTML}` + : ''; + + const { translations } = orthography; + const translationsHTML = `

Translations
${translations.map(translation => { + translation = translation.split('=').map(value => value.trim()); + if (translation.length > 1 && translation[0] !== '' && translation[1] !== '') { + return `${translation[0]}${translation[1]}`; + } + return false; + }).filter(html => html !== false).join(' ')}

`; + const orthographyNotesHTML = orthography.notes.trim().length > 0 ? '

Notes
' + md(removeTags(orthography.notes)) + '' : ''; + const orthographyHTML = translations.length > 0 && orthographyNotesHTML.length > 0 + ? `

Orthography

+ ${translations.length > 0 ? translationsHTML : ''} + ${orthographyNotesHTML}` + : ''; + const grammarHTML = '

Grammar

' + md(removeTags(grammar.notes)) + '
'; + + detailsPanel.innerHTML = generalHTML + phonologyHTML + phonotacticsHTML + orthographyHTML + grammarHTML; +} + +export function renderStats() { + const wordStats = getWordsStats(); + const numberOfWordsHTML = `

Number of Words
${wordStats.numberOfWords.map(stat => `${stat.name}${stat.value}`).join(' ')}

`; + const wordLengthHTML = `

Word Length
Shortest${wordStats.wordLength.shortest} + Longest${wordStats.wordLength.longest} + Average${wordStats.wordLength.average}

`; + const letterDistributionHTML = `

Letter Distribution
${wordStats.letterDistribution.map(stat => `${stat.letter}${stat.percentage.toFixed(2)}`).join(' ')}

`; + const totalLettersHTML = `

${wordStats.totalLetters} Total Letters

`; + + detailsPanel.innerHTML = numberOfWordsHTML + wordLengthHTML + letterDistributionHTML + totalLettersHTML; +} + +export function renderPartsOfSpeech(onlyOptions = false) { + let optionsHTML = '', + searchHTML = ''; + window.currentDictionary.partsOfSpeech.forEach(partOfSpeech => { + partOfSpeech = removeTags(partOfSpeech); + optionsHTML += ``; + searchHTML += ``; + }); + searchHTML += `Check All Uncheck All`; + + Array.from(document.getElementsByClassName('part-of-speech-select')).forEach(select => { + const selectedValue = select.value; + select.innerHTML = optionsHTML; + select.value = selectedValue; + }); + if (!onlyOptions) { + document.getElementById('searchPartsOfSpeech').innerHTML = searchHTML; + } + + setupSearchFilters(); +} \ No newline at end of file diff --git a/src/js/render/index.js b/src/js/render/index.js new file mode 100644 index 0000000..d2c8c52 --- /dev/null +++ b/src/js/render/index.js @@ -0,0 +1,14 @@ +import { renderDictionaryDetails, renderPartsOfSpeech } from './details'; +import { renderWords } from './words'; + +export function renderAll() { + renderTheme(); + renderDictionaryDetails(); + renderPartsOfSpeech(); + renderWords(); +} + +export function renderTheme() { + const { theme } = window.currentDictionary.settings; + document.body.id = theme + 'Theme'; +} diff --git a/src/js/render/modals.js b/src/js/render/modals.js new file mode 100644 index 0000000..ea96bda --- /dev/null +++ b/src/js/render/modals.js @@ -0,0 +1,74 @@ +import { + setupMaximizeModal, + setupInfoModal, + setupIPATable, + setupIPAFields +} from '../setupListeners'; +import ipaTableFile from '../KeyboardFire/phondue/ipa-table.html'; + +export function renderIPAHelp() { + import('../KeyboardFire/phondue/usage.html').then(html => { + renderInfoModal(html); + }); +} + +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 = ` + `; + + document.body.appendChild(modalElement); + + setupIPAFields(); + setupIPATable(modalElement, textBox); +} + +export function renderMaximizedTextbox(maximizeButton) { + maximizeButton = typeof maximizeButton.target === 'undefined' || maximizeButton.target === '' ? maximizeButton : maximizeButton.target; + const label = maximizeButton.parentElement.innerText.replace(/(\*|Maximize)/g, '').trim(); + const textBox = maximizeButton.parentElement.querySelector('textarea'); + const modalElement = document.createElement('section'); + modalElement.classList.add('modal', 'maximize-modal'); + modalElement.innerHTML = ` + `; + + document.body.appendChild(modalElement); + + setupMaximizeModal(modalElement, textBox); +} + +export function renderInfoModal(content) { + const modalElement = document.createElement('section'); + modalElement.classList.add('modal', 'info-modal'); + modalElement.innerHTML = ` + `; + + document.body.appendChild(modalElement); + + setupInfoModal(modalElement); +} diff --git a/src/js/render/words.js b/src/js/render/words.js new file mode 100644 index 0000000..bb37206 --- /dev/null +++ b/src/js/render/words.js @@ -0,0 +1,172 @@ +import md from 'marked'; +import { removeTags } from '../../helpers'; +import { getHomonymnNumber, hasToken } from '../utilities'; +import { getMatchingSearchWords, highlightSearchTerm, getSearchFilters, getSearchTerm } from '../search'; +import { + setupWordOptionButtons, + setupPagination, + setupWordOptionSelections, + setupWordEditFormButtons, +} from '../setupListeners'; +import { getPaginationData } from '../pagination'; +import { getOpenEditForms, translateOrthography, parseReferences } from '../wordManagement'; +import { renderAd } from '../ads'; +import { getPublicLink } from '../account/utilities'; +import { renderPartsOfSpeech } from './details'; + +export function renderWords() { + let wordsHTML = ''; + let openEditForms = getOpenEditForms(); + let words = false; + const isPublic = hasToken() && window.currentDictionary.settings.isPublic; + + if (window.currentDictionary.words.length === 0) { + wordsHTML = `
+
+

No Words Created

+
+
+
Use the Word Form to create words or click the Help button below!
+
+
`; + } else { + words = getMatchingSearchWords(); + + if (words.length === 0) { + wordsHTML = `
+
+

No Search Results

+
+
+
Edit your search or filter to show words.
+
+
`; + } + + if (openEditForms.length > 0) { + // Clone the dom nodes + openEditForms.forEach((wordFormId, index) => { + openEditForms[index] = document.getElementById(wordFormId.toString()).cloneNode(true); + }); + } + + // const { pageStart, pageEnd } = getPaginationData(words); + + // words.slice(pageStart, pageEnd).forEach(originalWord => { + words.forEach((originalWord, displayIndex) => { + const word = highlightSearchTerm({ + name: removeTags(originalWord.name), + pronunciation: removeTags(originalWord.pronunciation), + partOfSpeech: removeTags(originalWord.partOfSpeech), + definition: removeTags(originalWord.definition), + details: parseReferences(removeTags(originalWord.details)), + wordId: originalWord.wordId, + }); + const homonymnNumber = getHomonymnNumber(originalWord); + const shareLink = window.currentDictionary.hasOwnProperty('externalID') ? getPublicLink() + '/' + word.wordId : ''; + + wordsHTML += renderAd(displayIndex); + + let wordNameDisplay = translateOrthography(word.name); + + wordsHTML += `
+
+

${wordNameDisplay}${homonymnNumber > 0 ? ' ' + homonymnNumber.toString() + '' : ''}

+ ${word.pronunciation} + ${word.partOfSpeech} + ${isPublic ? `` : ''} + Options + +
+
+
${word.definition}
+
+ ${md(word.details)} +
+
+
`; + }); + } + + document.getElementById('entries').innerHTML = wordsHTML; + + if (openEditForms.length > 0) { + // Clone the dom nodes + openEditForms.forEach(editForm => { + const entryElement = document.getElementById(editForm.id); + entryElement.parentNode.replaceChild(editForm, entryElement); + }); + setupWordEditFormButtons(); + } + + setupWordOptionButtons(); + setupWordOptionSelections(); + + // Show Search Results + const searchTerm = getSearchTerm(); + const filters = getSearchFilters(); + let resultsText = searchTerm !== '' || !filters.allPartsOfSpeechChecked ? (words ? words.length : 0).toString() + ' Results' : ''; + resultsText += !filters.allPartsOfSpeechChecked ? ' (Filtered)' : ''; + document.getElementById('searchResults').innerHTML = resultsText; + + // renderPagination(words); +} + +export function renderPagination(filteredWords) { + const paginationData = getPaginationData(filteredWords); + + if (paginationData.pages > 0) { + let paginationHTML = (paginationData.currentPage > 0 ? '« Previous' : '') + + '' + + (paginationData.currentPage < paginationData.pages - 1 ? 'Next »' : ''); + + Array.from(document.getElementsByClassName('pagination')).forEach(pagination => { + pagination.innerHTML = paginationHTML; + }); + + setupPagination(); + } +} + +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 = `
+ Field Help`; + const plainPronunciationField = ``; + const editForm = `
+ + + + + +
+ Save Changes + Cancel Edit +
`; + + document.getElementById(wordId.toString()).innerHTML = editForm; + setupWordEditFormButtons(); + renderPartsOfSpeech(true); + } +} diff --git a/src/js/search.js b/src/js/search.js index cecf2b9..a1834ef 100644 --- a/src/js/search.js +++ b/src/js/search.js @@ -1,6 +1,6 @@ import { cloneObject, getIndicesOf } from "../helpers"; import removeDiacritics from "./StackOverflow/removeDiacritics"; -import { renderWords } from "./render"; +import { renderWords } from "./render/words"; export function showSearchModal() { document.getElementById('searchModal').style.display = 'block'; diff --git a/src/js/settings.js b/src/js/settings.js index 5d14499..26c9bff 100644 --- a/src/js/settings.js +++ b/src/js/settings.js @@ -1,7 +1,7 @@ import { SETTINGS_KEY, DEFAULT_SETTINGS } from "../constants"; import { cloneObject, removeTags } from "../helpers"; import { usePhondueDigraphs } from "./KeyboardFire/phondue/ipaField"; -import { renderWords } from "./render"; +import { renderWords } from "./render/words"; import { addMessage, hasToken } from "./utilities"; import { enableHotKeys, disableHotKeys } from "./hotkeys"; diff --git a/src/js/setupListeners.js b/src/js/setupListeners.js index e4b8620..8c0f6a8 100644 --- a/src/js/setupListeners.js +++ b/src/js/setupListeners.js @@ -1,5 +1,6 @@ import {showSection, hideDetailsPanel} from './displayToggles'; -import { renderWords, renderEditForm, renderMaximizedTextbox, renderInfoModal, renderIPATable, renderIPAHelp } from './render'; +import { renderWords, renderEditForm } from './render/words'; +import { renderMaximizedTextbox, renderInfoModal, renderIPATable, renderIPAHelp } from './render/modals'; import { confirmEditWord, cancelEditWord, confirmDeleteWord, submitWordForm } from './wordManagement'; import { openEditModal, saveEditModal, saveAndCloseEditModal, exportDictionary, exportWords, importDictionary, importWords, confirmDeleteDictionary } from './dictionaryManagement'; import { goToNextPage, goToPreviousPage, goToPage } from './pagination'; diff --git a/src/js/wordManagement.js b/src/js/wordManagement.js index a4131e6..5974149 100644 --- a/src/js/wordManagement.js +++ b/src/js/wordManagement.js @@ -1,4 +1,4 @@ -import { renderWords } from "./render"; +import { renderWords } from "./render/words"; import { wordExists, addMessage, getNextId, hasToken, getHomonymnIndexes } from "./utilities"; import removeDiacritics from "./StackOverflow/removeDiacritics"; import { removeTags, getTimestampInSeconds } from "../helpers"; From 4e1ee35c5b01f47be8b18f43ac87b891cf5998d0 Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Mon, 8 Jul 2019 16:17:16 -0600 Subject: [PATCH 112/134] Fix when orthography section displays --- src/js/render/details.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/render/details.js b/src/js/render/details.js index 64c33ab..f1e4b97 100644 --- a/src/js/render/details.js +++ b/src/js/render/details.js @@ -93,7 +93,7 @@ export function renderDetails() { return false; }).filter(html => html !== false).join(' ')}

`; const orthographyNotesHTML = orthography.notes.trim().length > 0 ? '

Notes
' + md(removeTags(orthography.notes)) + '' : ''; - const orthographyHTML = translations.length > 0 && orthographyNotesHTML.length > 0 + const orthographyHTML = translations.length > 0 || orthographyNotesHTML.length > 0 ? `

Orthography

${translations.length > 0 ? translationsHTML : ''} ${orthographyNotesHTML}` From 2bf0f15f675bbeb0e376b5e1d411f872affc7f06 Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Tue, 9 Jul 2019 12:24:40 -0600 Subject: [PATCH 113/134] Fix how custom sorting works! Sort alphabetically first, then by custom if specified --- src/js/wordManagement.js | 106 +++++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 49 deletions(-) diff --git a/src/js/wordManagement.js b/src/js/wordManagement.js index 5974149..db36044 100644 --- a/src/js/wordManagement.js +++ b/src/js/wordManagement.js @@ -35,57 +35,65 @@ export function sortWords(render) { const { alphabeticalOrder } = window.currentDictionary; const sortBy = sortByDefinition ? 'definition' : 'name'; - const ordering = {}; // map for efficient lookup of sortIndex - for (let i = 0; i < alphabeticalOrder.length; i++) { - ordering[alphabeticalOrder[i]] = i; - } - window.currentDictionary.words.sort((wordA, wordB) => { - if (wordA[sortBy] === wordB[sortBy]) return 0; - - const aLetters = wordA[sortBy].split(''); - const bLetters = wordB[sortBy].split(''); - - for (let i = 0; i < aLetters.length; i++) { - const a = aLetters[i]; - if (ordering.hasOwnProperty(a)) { // if a is in the alphabet... - if (typeof bLetters[i] !== 'undefined') { // and if wordB has a letter at the same position... - const b = bLetters[i]; - if (ordering.hasOwnProperty(b)) { // and b is in the alphabet then compare the letters - const aIndex = ordering[a]; - const bIndex = ordering[b]; - if (aIndex === bIndex) { // If the letters are the same, then... - // If wordA is shorter than wordB and this is wordA's last letter, then sort a first; - if (aLetters.length < bLetters.length && i == aLetters.length - 1) return -1; - continue; // Otherwise if it is the same letter, check the next letter - } - return aIndex - bIndex; // If different and both in alphabet, compare alphabetical order - } - } else { - return 1; // If b is shorter than a after looping, then sort a after - } - } else if (ordering.hasOwnProperty(bLetters[i])) { - return 1; // If a is not in the alphabet but b is, sort a after - } else { - if (typeof bLetters[i] !== 'undefined') { // and if wordB has a letter at the same position... - const b = bLetters[i]; - if (removeDiacritics(a).toLowerCase() === removeDiacritics(b).toLowerCase()) { - if (aLetters.length < bLetters.length && i == aLetters.length - 1) return -1; - continue; - } - return removeDiacritics(a).toLowerCase() > removeDiacritics(b).toLowerCase() ? 1 : -1; - } else { - return 1; // If b is shorter than a after looping, then sort a after - } - } - } - - console.log('done looping, comparing normally:', wordA[sortBy], wordB[sortBy]); - - // If after looping, the alphabet is still different, just compare the words alphabetically. - if (removeDiacritics(wordA[sortBy]).toLowerCase() === removeDiacritics(wordB[sortBy]).toLowerCase()) return 0; - return removeDiacritics(wordA[sortBy]).toLowerCase() > removeDiacritics(wordB[sortBy]).toLowerCase() ? 1 : -1; + // Sort normally first + return wordA[sortBy].localeCompare(wordB[sortBy], 'en', { sensitivity: 'base' }); // This is the smart way to do the below! + // if (removeDiacritics(wordA[sortBy]).toLowerCase() === removeDiacritics(wordB[sortBy]).toLowerCase()) return 0; + // return removeDiacritics(wordA[sortBy]).toLowerCase() > removeDiacritics(wordB[sortBy]).toLowerCase() ? 1 : -1; }); + + if (alphabeticalOrder.length > 0) { + // If there's an alphabetical order specified, sort by that after! Any letters not in the alphabet will be unsorted, keeping them in ASCII order + const ordering = {}; // map for efficient lookup of sortIndex + for (let i = 0; i < alphabeticalOrder.length; i++) { + ordering[alphabeticalOrder[i]] = i + 1; // Add 1 to prevent 0 from resolving to false + } + + window.currentDictionary.words.sort((wordA, wordB) => { + // console.log(alphabeticalOrder, ordering); + // console.log('comparing:', wordA[sortBy], wordB[sortBy]); + if (wordA[sortBy] === wordB[sortBy]) return 0; + + const aLetters = wordA[sortBy].split(''); + const bLetters = wordB[sortBy].split(''); + + for (let i = 0; i < aLetters.length; i++) { + const a = aLetters[i]; + const b = bLetters[i]; + // console.log('comparing letters', a, b); + if (!b) { + // console.log('no b, ', wordA[sortBy], 'is longer than', wordB[sortBy]); + return 1; + } + if (!ordering[a] && !ordering[b]) { + // console.log('a and b not in dictionary:', a, b, 'continuing to the next letter'); + continue; + } + if (!ordering[a]) { + // console.log('a is not in dictionary:', a, 'moving back:', wordA[sortBy]); + return 1; + } + if (!ordering[b]) { + // console.log('b is not in dictionary:', b, 'moving forward:', wordA[sortBy]); + return -1; + } + if (ordering[a] === ordering[b]) { + // console.log('letters are the same order:', a, b); + if (aLetters.length < bLetters.length && i === aLetters.length - 1) { + // console.log(a, 'is shorter than', b); + return -1; + } + // console.log(a, 'is the same as', b, 'continuing to the next letter'); + continue; + } + // console.log('comparing order:', a, b, 'result:', ordering[a] - ordering[b]); + return ordering[a] - ordering[b]; + } + + // console.log('all of the letters were dumb, no sort'); + return 0; + }); + } saveDictionary(false); From aa956e9820426de764834c6bebf1f459bb66b459 Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Tue, 9 Jul 2019 13:06:03 -0600 Subject: [PATCH 114/134] Make alphabetical order editable --- src/js/dictionaryManagement.js | 11 +++++++---- template-index.html | 8 ++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/js/dictionaryManagement.js b/src/js/dictionaryManagement.js index 176fc31..373b8ce 100644 --- a/src/js/dictionaryManagement.js +++ b/src/js/dictionaryManagement.js @@ -13,7 +13,7 @@ export function updateDictionary () { } export function openEditModal() { - const { name, specification, description, partsOfSpeech } = window.currentDictionary; + const { name, specification, description, partsOfSpeech, alphabeticalOrder } = window.currentDictionary; const { consonants, vowels, blends } = window.currentDictionary.details.phonology; const { phonotactics, orthography, grammar } = window.currentDictionary.details; const { allowDuplicates, caseSensitive, sortByDefinition, theme, isPublic } = window.currentDictionary.settings; @@ -22,6 +22,7 @@ export function openEditModal() { document.getElementById('editSpecification').value = specification; document.getElementById('editDescription').value = description; document.getElementById('editPartsOfSpeech').value = partsOfSpeech.join(','); + document.getElementById('editAlphabeticalOrder').value = alphabeticalOrder.join(' '); document.getElementById('editConsonants').value = consonants.join(' '); document.getElementById('editVowels').value = vowels.join(' '); @@ -52,6 +53,7 @@ export function saveEditModal() { window.currentDictionary.specification = removeTags(document.getElementById('editSpecification').value.trim()); window.currentDictionary.description = removeTags(document.getElementById('editDescription').value.trim()); window.currentDictionary.partsOfSpeech = document.getElementById('editPartsOfSpeech').value.split(',').map(val => val.trim()).filter(val => val !== ''); + window.currentDictionary.alphabeticalOrder = document.getElementById('editAlphabeticalOrder').value.split(' ').map(val => val.trim()).filter(val => val !== ''); window.currentDictionary.details.phonology.consonants = document.getElementById('editConsonants').value.split(' ').map(val => val.trim()).filter(val => val !== ''); window.currentDictionary.details.phonology.vowels = document.getElementById('editVowels').value.split(' ').map(val => val.trim()).filter(val => val !== ''); @@ -75,14 +77,15 @@ export function saveEditModal() { } else { window.currentDictionary.settings.isPublic = false; } - - addMessage('Saved ' + window.currentDictionary.specification + ' Successfully'); - saveDictionary(); + renderTheme(); renderDictionaryDetails(); renderPartsOfSpeech(); sortWords(true); + addMessage('Saved ' + window.currentDictionary.specification + ' Successfully'); + saveDictionary(); + if (hasToken()) { import('./account/index.js').then(account => { account.uploadDetailsDirect(); diff --git a/template-index.html b/template-index.html index 8ac892d..d36e001 100644 --- a/template-index.html +++ b/template-index.html @@ -255,8 +255,12 @@ - +

Phonotactics

From f9fa6a178d4fc95ff22b66c1de41fd94cfd81c32 Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Wed, 10 Jul 2019 13:23:32 -0600 Subject: [PATCH 119/134] Stop dictionary update if name or specification blank --- src/js/dictionaryManagement.js | 6 ++++++ src/scss/_structure.scss | 2 +- src/scss/mobile/_structure.scss | 2 +- template-index.html | 2 ++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/js/dictionaryManagement.js b/src/js/dictionaryManagement.js index 800b60d..aeebc14 100644 --- a/src/js/dictionaryManagement.js +++ b/src/js/dictionaryManagement.js @@ -54,7 +54,13 @@ export function saveEditModal() { const updatedDictionary = cloneObject(window.currentDictionary); delete updatedDictionary.words; updatedDictionary.name = removeTags(document.getElementById('editName').value.trim()); + if (updatedDictionary.name.length < 1) { + updatedDictionary.name = window.currentDictionary.name; + } updatedDictionary.specification = removeTags(document.getElementById('editSpecification').value.trim()); + if (updatedDictionary.specification.length < 1) { + updatedDictionary.specification = window.currentDictionary.specification; + } updatedDictionary.description = removeTags(document.getElementById('editDescription').value.trim()); updatedDictionary.partsOfSpeech = document.getElementById('editPartsOfSpeech').value.split(',').map(val => val.trim()).filter(val => val !== ''); updatedDictionary.alphabeticalOrder = document.getElementById('editAlphabeticalOrder').value.split(' ').map(val => val.trim()).filter(val => val !== ''); diff --git a/src/scss/_structure.scss b/src/scss/_structure.scss index 983a34a..c9598dd 100644 --- a/src/scss/_structure.scss +++ b/src/scss/_structure.scss @@ -279,7 +279,7 @@ $nav-font-height: 16px; #editDescription { width: 100%; - height: 280px; + height: 240px; } } diff --git a/src/scss/mobile/_structure.scss b/src/scss/mobile/_structure.scss index 5b6f7c5..0e85f26 100644 --- a/src/scss/mobile/_structure.scss +++ b/src/scss/mobile/_structure.scss @@ -89,7 +89,7 @@ $mobile-word-form-size: 32px; } #editDescription { width: 100%; - height: 260px; + height: 220px; } } diff --git a/template-index.html b/template-index.html index 21a3667..2ce61dd 100644 --- a/template-index.html +++ b/template-index.html @@ -245,9 +245,11 @@
+
@@ -241,9 +244,11 @@
Maximize
@@ -254,8 +259,12 @@ -
+

Phonotactics

@@ -309,6 +321,13 @@

Orthography

+ @@ -345,6 +364,9 @@ +