From d1b123317f3168221314f4565ee750f8d3d29ffd Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Fri, 24 May 2019 18:50:31 -0600 Subject: [PATCH] Start non-invasive work on public viewing --- package.json | 2 +- src/js/view/dictionaryManagement.js | 12 + src/js/view/index.js | 20 ++ src/js/view/render.js | 199 +++++++++++++++++ src/js/view/setupListeners.js | 108 +++++++++ src/php/api/Dictionary.php | 74 +++++- src/php/api/index.php | 21 ++ view.html | 334 ++++++++++++++++++++++++++++ 8 files changed, 768 insertions(+), 2 deletions(-) create mode 100644 src/js/view/dictionaryManagement.js create mode 100644 src/js/view/index.js create mode 100644 src/js/view/render.js create mode 100644 src/js/view/setupListeners.js create mode 100644 view.html diff --git a/package.json b/package.json index 943a791..de02b76 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "license": "UNLICENCED", "scripts": { "start": "concurrently \"npm run watch-js\" \"npm run watch-php\"", - "watch-js": "parcel watch index.html --public-url ./", + "watch-js": "parcel watch index.html view.html --public-url ./", "watch-php": "cpx \"src/php/**/*\" dist -v -w", "bundle": "parcel build index.html && cpx src/php/**/* dist", "serve-frontend-only": "parcel index.html", diff --git a/src/js/view/dictionaryManagement.js b/src/js/view/dictionaryManagement.js new file mode 100644 index 0000000..eb5f3fb --- /dev/null +++ b/src/js/view/dictionaryManagement.js @@ -0,0 +1,12 @@ +export function getDictionary() { + const url = window.location.href.replace(/\#.*$/gi, ''); + console.log(url); + let dict = url.substr(url.lastIndexOf('?')); + console.log(dict); + if (dict === url) { + dict = dict.substr(dict.lastIndexOf('/')); + console.log(dict); + } + dict = dict.replace(/[\?\/]/g, ''); + console.log(dict); +} \ No newline at end of file diff --git a/src/js/view/index.js b/src/js/view/index.js new file mode 100644 index 0000000..ca3f0b9 --- /dev/null +++ b/src/js/view/index.js @@ -0,0 +1,20 @@ +import '../../main.scss'; +import { getDictionary } from './dictionaryManagement'; + +// import setupListeners, { setupSearchFilters } from './js/setupListeners'; +// import { renderAll } from './js/render'; +// import { hasToken } from './js/utilities'; +// import { loadDictionary } from './js/dictionaryManagement'; +// import { loadSettings } from './js/settings'; + +function initialize() { + getDictionary(); + // setupSearchFilters(); +} + +window.onload = (function (oldLoad) { + return function () { + oldLoad && oldLoad(); + initialize(); + } +})(window.onload); \ No newline at end of file diff --git a/src/js/view/render.js b/src/js/view/render.js new file mode 100644 index 0000000..7082d23 --- /dev/null +++ b/src/js/view/render.js @@ -0,0 +1,199 @@ +import md from 'marked'; +import { removeTags, slugify } from '../../helpers'; +import { getWordsStats, wordExists } from '../utilities'; +import { getMatchingSearchWords, highlightSearchTerm, getSearchFilters, getSearchTerm } from '../search'; +import { showSection } from '../displayToggles'; +import { setupSearchFilters, setupInfoModal } from './setupListeners'; + +export function renderAll() { + renderDictionaryDetails(); + renderPartsOfSpeech(); + renderWords(); +} + +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); + document.getElementById('dictionaryName').innerHTML = dictionaryName; +} + +export function renderDescription() { + const descriptionHTML = md(removeTags(window.currentDictionary.description)); + + document.getElementById('detailsPanel').innerHTML = '
' + descriptionHTML + '
'; +} + +export function renderDetails() { + const { partsOfSpeech, alphabeticalOrder } = window.currentDictionary; + const { phonology, 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, phonotactics } = 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, exceptions } = 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 exceptionsHTML = exceptions.trim().length > 0 ? '

Exceptions:

' + md(removeTags(exceptions)) + '
' : ''; + const phonotacticsHTML = `

Phonotactics

+
+
${onsetHTML}
+
${nucleusHTML}
+
${codaHTML}
+
+ ${exceptionsHTML}`; + + const orthographyHTML = '

Orthography

Notes:

' + md(removeTags(orthography.notes)) + '
'; + const grammarHTML = '

Grammar

Notes:

' + 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 words = false; + + if (window.currentDictionary.words.length === 0) { + wordsHTML = `
+
+

No Words Found

+
+
+
Either this dictionary has not yet been started, or something prevented words from downloading.
+
+
`; + } else { + words = getMatchingSearchWords(); + + if (words.length === 0) { + wordsHTML = `
+
+

No Search Results

+
+
+
Edit your search or filter to show words.
+
+
`; + } + + 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); + } + }); + } + const word = highlightSearchTerm({ + name: removeTags(originalWord.name), + pronunciation: removeTags(originalWord.pronunciation), + partOfSpeech: removeTags(originalWord.partOfSpeech), + definition: removeTags(originalWord.definition), + details: detailsMarkdown, + wordId: originalWord.wordId, + }); + wordsHTML += `
+
+

${word.name}

+ ${word.pronunciation} + ${word.partOfSpeech} +
+
+
${word.definition}
+
+ ${md(word.details)} +
+
+
`; + }); + } + + document.getElementById('entries').innerHTML = wordsHTML; + + // 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; +} + +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/view/setupListeners.js b/src/js/view/setupListeners.js new file mode 100644 index 0000000..8b5bd59 --- /dev/null +++ b/src/js/view/setupListeners.js @@ -0,0 +1,108 @@ +import {showSection, hideDetailsPanel} from '../displayToggles'; +import { showSearchModal, clearSearchText, checkAllPartsOfSpeechFilters, uncheckAllPartsOfSpeechFilters } from '../search'; +import { renderWords, renderInfoModal } from './render'; + +export default function setupListeners() { + setupDetailsTabs(); + setupSearchBar(); + setupInfoButtons(); +} + +function setupDetailsTabs() { + const tabs = document.querySelectorAll('#detailsSection nav li'); + tabs.forEach(tab => { + tab.addEventListener('click', () => { + const section = tab.innerText.toLowerCase(); + const isActive = tab.classList.contains('active'); + tabs.forEach(t => t.classList.remove('active')); + if (isActive) { + hideDetailsPanel(); + } else { + tab.classList.add('active'); + showSection(section); + } + }); + }); + setupEditFormTabs(); + setupEditFormInteractions(); + setupEditFormButtons(); +} + +function setupSearchBar() { + const searchBox = document.getElementById('searchBox'), + clearSearchButton = document.getElementById('clearSearchButton'), + openSearchModal = document.getElementById('openSearchModal'), + searchIgnoreDiacritics = document.getElementById('searchIgnoreDiacritics'), + searchExactWords = document.getElementById('searchExactWords'), + searchIncludeDetails = document.getElementById('searchIncludeDetails'); + searchBox.addEventListener('change', () => { + renderWords(); + }); + searchBox.addEventListener('input', event => { + openSearchModal.value = event.target.value; + }); + clearSearchButton.addEventListener('click', clearSearchText); + openSearchModal.addEventListener('click', showSearchModal); + + const toggleDetailsCheck = function() { + if (searchExactWords.checked) { + searchIncludeDetails.checked = false; + searchIncludeDetails.disabled = true; + } else { + searchIncludeDetails.disabled = false; + searchIncludeDetails.checked = true; + } + } + + searchIgnoreDiacritics.addEventListener('change', () => { + if (searchIgnoreDiacritics.checked) { + searchExactWords.checked = false; + searchExactWords.disabled = true; + } else { + searchExactWords.disabled = false; + } + toggleDetailsCheck(); + }); + + searchExactWords.addEventListener('change', () => toggleDetailsCheck()); +} + +export function setupSearchFilters() { + const searchFilters = document.querySelectorAll('#searchOptions input[type="checkbox"]'), + searchBox = document.getElementById('searchBox'); + Array.from(searchFilters).concat([searchBox]).forEach(filter => { + filter.removeEventListener('change', renderWords); + filter.addEventListener('change', renderWords); + }); + document.getElementById('checkAllFilters').removeEventListener('click', checkAllPartsOfSpeechFilters); + document.getElementById('checkAllFilters').addEventListener('click', checkAllPartsOfSpeechFilters); + document.getElementById('uncheckAllFilters').removeEventListener('click', uncheckAllPartsOfSpeechFilters); + document.getElementById('uncheckAllFilters').addEventListener('click', uncheckAllPartsOfSpeechFilters); +} + +export function setupInfoButtons() { + document.getElementById('helpInfoButton').addEventListener('click', () => { + import('../markdown/help.md').then(html => { + renderInfoModal(html); + }); + }); + document.getElementById('termsInfoButton').addEventListener('click', () => { + import('../markdown/terms.md').then(html => { + renderInfoModal(html); + }); + }); + document.getElementById('privacyInfoButton').addEventListener('click', () => { + import('../markdown/privacy.md').then(html => { + renderInfoModal(html); + }); + }); +} + +export function setupInfoModal(modal) { + const closeElements = modal.querySelectorAll('.modal-background, .close-button'); + Array.from(closeElements).forEach(close => { + close.addEventListener('click', () => { + modal.parentElement.removeChild(modal); + }); + }); +} diff --git a/src/php/api/Dictionary.php b/src/php/api/Dictionary.php index c15a76e..6b92ebf 100644 --- a/src/php/api/Dictionary.php +++ b/src/php/api/Dictionary.php @@ -88,6 +88,78 @@ VALUES ($new_id, ?, ?, ?, ?)"; return array(); } + public function getPublicDictionaryDetails ($dictionary_hash) { + $dictionary = $this->token->unhash($dictionary_hash); + if ($dictionary !== false) { + $query = "SELECT * FROM dictionaries JOIN dictionary_linguistics ON dictionary = id WHERE id=? AND is_public=1"; + $result = $this->db->query($query, array($dictionary))->fetch(); + if ($result) { + // Default json values in case they are somehow not created by front end first + $partsOfSpeech = $result['parts_of_speech'] !== '' ? $result['parts_of_speech'] : $this->defaults['partsOfSpeech']; + + return array( + 'externalID' => $this->token->hash($result['id']), + 'name' => $result['name'], + 'specification' => $result['specification'], + 'description' => $result['description'], + 'partsOfSpeech' => explode(',', $partsOfSpeech), + 'details' => array( + 'phonology' => array( + 'consonants' => $result['consonants'] !== '' ? explode(' ', $result['consonants']) : array(), + 'vowels' => $result['vowels'] !== '' ? explode(' ', $result['vowels']) : array(), + 'blends' => $result['blends'] !== '' ? explode(' ', $result['blends']) : array(), + 'phonotactics' => array( + 'onset' => $result['onset'] !== '' ? explode(',', $result['onset']) : array(), + 'nucleus' => $result['nucleus'] !== '' ? explode(',', $result['nucleus']) : array(), + 'coda' => $result['coda'] !== '' ? explode(',', $result['coda']) : array(), + 'exceptions' => $result['exceptions'], + ), + ), + 'orthography' => array( + 'notes' => $result['orthography_notes'], + ), + 'grammar' => array( + 'notes' => $result['grammar_notes'], + ), + ), + 'settings' => array( + 'allowDuplicates' => $result['allow_duplicates'] === '1' ? true : false, + 'caseSensitive' => $result['case_sensitive'] === '1' ? true : false, + 'sortByDefinition' => $result['sort_by_definition'] === '1' ? true : false, + 'isComplete' => false, + 'isPublic' => $result['is_public'] === '1' ? true : false, + ), + 'lastUpdated' => is_null($result['last_updated']) ? $result['created_on'] : $result['last_updated'], + 'createdOn' => $result['created_on'], + ); + } + } + return false; + } + + public function getPublicDictionaryWords ($dictionary_hash) { + $dictionary = $this->token->unhash($dictionary_hash); + if ($dictionary !== false) { + $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( + 'name' => $row['name'], + 'pronunciation' => $row['pronunciation'], + 'partOfSpeech' => $row['part_of_speech'], + 'definition' => $row['definition'], + 'details' => $row['details'], + 'lastUpdated' => is_null($row['last_updated']) ? intval($row['created_on']) : intval($row['last_updated']), + 'createdOn' => intval($row['created_on']), + 'wordId' => 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(); @@ -211,7 +283,7 @@ WHERE dictionary=$dictionary"; 'partOfSpeech' => $row['part_of_speech'], 'definition' => $row['definition'], 'details' => $row['details'], - 'lastUpdated' => is_null($row['last_updated']) ? null : intval($row['last_updated']), + 'lastUpdated' => is_null($row['last_updated']) ? intval($row['created_on']) : intval($row['last_updated']), 'createdOn' => intval($row['created_on']), 'wordId' => intval($row['word_id']), ); diff --git a/src/php/api/index.php b/src/php/api/index.php index ce8bc08..613be9a 100644 --- a/src/php/api/index.php +++ b/src/php/api/index.php @@ -218,6 +218,27 @@ switch ($action) { 'error' => true, ), 400); } + case 'get-public-dictionary': { + if (isset($request['dictionary'])) { + $dictionary = new Dictionary(); + $dictionary_data = $dictionary->getPublicDictionaryDetails($request['dictionary']); + if ($dictionary_data !== false) { + $dictionary_data['words'] = $dictionary->getPublicDictionaryWords($request['dictionary']); + return Response::json(array( + 'data' => $dictionary_data, + 'error' => false, + ), 200); + } + return Response::json(array( + 'data' => 'Could not get dictionary: invalid id', + 'error' => true, + ), 401); + } + return Response::json(array( + 'data' => 'Could not get dictionary: no id provided', + 'error' => true, + ), 400); + } case 'set-whole-current-dictionary': { if ($token !== false && isset($request['dictionary'])) { $user = new User(); diff --git a/view.html b/view.html new file mode 100644 index 0000000..b1fd0c9 --- /dev/null +++ b/view.html @@ -0,0 +1,334 @@ + + + + + + + Lexiconga + + + +
+

Lexiconga

+ + + + + +
+
+ +
+ + +
+
+

Dictionary Name

+ + +
+ +
+ +
+
+
+

Loading Words

+
+
+
Please Wait...
+
+
+
+ +
+
+
+ + + + + + + +
+ + \ No newline at end of file