Compare commits

...

No commits in common. "May-26-2016" and "master" have entirely different histories.

140 changed files with 20588 additions and 6172 deletions

10
.gitignore vendored
View File

@ -1,2 +1,8 @@
php/google/
ipa_character_picker/
node_modules/
.cache/
dist/
vendor/
processedImages/
src/php/api/config.php
src/php/api/migrate.php

View File

@ -1,3 +0,0 @@
RewriteEngine On # Turn on the rewriting engine
RewriteRule ^view/([0-9]+)/?$ view/index.php?dict=$1 [NC,L] # Handle dictionary ids.
RewriteRule ^([0-9]+)/?$ view/index.php?dict=$1 [NC,L] # Handle dictionary ids.

3
.htmlnanorc Normal file
View File

@ -0,0 +1,3 @@
{
"minifySvg": false
}

12
.postcssrc Normal file
View File

@ -0,0 +1,12 @@
{
"plugins": {
"autoprefixer": {
"overrideBrowserslist": [
">1%",
"last 4 versions",
"Firefox ESR",
"not ie < 9"
]
}
}
}

View File

@ -1,10 +0,0 @@
<div class="settingsCol"><form id="forgotForm" method="post" action="?forgot">
<h2>Forgot Password</h2>
<label><span>Email</span>
<input type="email" id="forgotEmailField" name="email" />
</label>
<div id="forgotError" style="font-weight:bold;color:red;"></div>
<button type="submit" id="forgotSubmitButton" onclick="ValidateForgot(); return false;">Email Password Reset Key</button>
<div id="dictionaryWarn"></div>
<span id="forgotPassword" class="clickable" onclick="ShowInfo('loginForm')">Log In/Create Account</span>
</form></div>

View File

@ -1,36 +0,0 @@
<div class="settingsCol"><form id="loginForm" method="post" action="?login">
<h2>Log In</h2>
<label><span>Email</span>
<input type="email" id="loginEmailField" name="email" />
</label>
<label><span>Password</span>
<input type="password" id="loginPasswordField" name="password" />
</label>
<div id="loginError" style="font-weight:bold;color:red;"></div>
<button type="submit" id="loginSubmitButton" onclick="ValidateLogin(); return false;">Log In</button>
<div id="dictionaryWarn" style="margin-bottom: 5px;"></div>
<span id="forgotPassword" class="clickable" onclick="ShowInfo('forgotForm')" style="margin-top:20px;">Forgot Password?</span>
</form></div>
<div class="settingsCol"><form id="createAccountForm" method="post" action="?createaccount">
<h2>Create a New Account</h2>
<p style="font-size: 12px;">Creating an account allows you to save and switch between as many dictionaries as you need and access them from any device for free! If you have a dictionary you've been working on loaded already, it will automatically be uploaded to your account when you log in for the first time.</p>
<p style="font-size: 12px;">Plus if you allow us to send you emails, we'll make sure that you're the first to hear about any new features that get added or if any of our policies change for any reason. We'll never spam you or sell your information.</p>
<p style="font-size: 12px;">By creating an account, you are indicating that you agree to the <span class="clickable" onclick="ShowInfo('termsText')" style="font-size:11px;vertical-align:top;background:#e0c19c;padding:4px 7px;">Terms of Service</span> and that you understand Lexiconga's <span class="clickable" onclick="ShowInfo('privacyText')" style="font-size:11px;vertical-align:top;background:#e0c19c;padding:4px 7px;">Privacy Policy</span>.</p>
<label><span>Email</span>
<input type="email" id="createAccountEmailField" name="email" />
</label>
<label><span>Password</span>
<input type="password" id="createAccountPasswordField" name="password" />
</label>
<label><span>Confirm Password</span>
<input type="password" id="createAccountPasswordConfirmField" name="confirmpassword" />
</label>
<label><span>Public Name <span class="clickable" onclick="ExplainPublicName()" style="font-size:11px;vertical-align:top;background:#e0c19c;padding:4px 7px;">?</span></span>
<input type="text" id="createAccountPublicNameField" name="publicname" />
</label>
<label style="display:inline;"><b>Allow Emails</b>
<input type="checkbox" id="createAccountAllowEmailsField" name="allowemails" checked="checked" />
</label> <span class="clickable" onclick="ExplainAllowEmails()" style="font-size:11px;vertical-align:top;background:#e0c19c;padding:4px 7px;">?</span>
<div id="createAccountError" style="font-weight:bold;color:red;"></div>
<button type="submit" id="createAccountSubmitButton" onclick="ValidateCreateAccount(); return false;">Create Account</button>
</form></div>

202
README.md
View File

@ -1,191 +1,47 @@
# Lexiconga Dictionary Builder Help
## Table of Contents
* [What is Lexiconga Dictionary Builder?](#what-is-lexiconga-dictionary-builder-)
* [How do I use Lexiconga?](#how-do-i-use-lexiconga-)
* [Getting Started](#getting-started)
* [Locking/Unlocking the Word Entry Form](#locking-unlocking-the-word-entry-form)
* [Viewing your Dictionary's Details](#viewing-your-dictionary-s-details)
* [Maximizing Large Text Boxes](#maximizing-large-text-boxes)
* [Entry Management](#entry-management)
* [The Settings Menu](#the-settings-menu)
* [Search/Filter](#search-filter)
* [Keyboard Shortcuts](#keyboard-shortcuts)
* [Accounts](#accounts)
* [Creating An Account](#creating-an-account)
* [Logging In](#logging-in)
* [Differences](#differences)
* [Account Settings](#account-settings)
* [Dictionary Settings](#dictionary-settings)
* [Public Dictionaries](#public-dictionaries)
* [Forgot Your Password?](#forgot-your-password-)
* [Lockout](#lockout)
* [Problems or Requests](#problems-or-requests)
* [Update Log](#update-log)
* [Future Plans](#future-plans)
* [Thanks](#thanks-)
* [Libraries Used](#libraries-used)
# Lexiconga
## What is Lexiconga Dictionary Builder?
Lexiconga is a tool intended to help you build constructed language (conlang) dictionaries/lexicons.
This is the light-as-possible rewrite of Lexiconga.
You can enter words and definitions, and they will appear nicely formatted and in alphabetical order under your dictionary's title, where you can also sort them by part of speech. If the default parts of speech are not adequate for your conlang, you can change them to whatever parts of speech you might need. You can even enter a description or full set of language rules that you can toggle on and off below the dictionary's title!
## Installation
It accepts Unicode characters so you can utilize whatever typable characters you might need and [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) for formatting long text entries, and if you want to share or even just make a backup of your dictionary, you can export it to a single convenient file that can be easily re-imported. Your dictionary is saved to your browser's localStorage every time you make a change, which means as long as you use the same browser and don't deliberately delete it (by clearing your cache), your dictionary will always be there when you come back.
0. Some dev tools require build tools.
- On Windows, install them with `npm install --global windows-build-tools`.
- Alternatively, you can just install the newest Python and Visual Studio (with Desktop C++ devkit).
1. Clone and run `yarn` and `composer install` to install dependencies.
1. Import `src/structure.sql` into a database called 'lexiconga' on your MariaDB server to get the database structure.
1. Copy `src/php/api/config.php.changeme` to `src/php/api/config.php` and update the values within to enable connections to your lexiconga database.
If you would like an added layer of accessibility and security (in case you clear your browser cache frequently), you can create an account, where you can store and switch between as many dictionaries as you need. Having an account will also allow you to access your dictionaries from any browser by logging in. (Just be careful you don't overwrite dictionaries by logging in and saving from separate locations!)
### Requirements
## How do I use Lexiconga?
* [Yarn](https://yarnpkg.com/) 1.12.3+
* [PHP](https://php.net/) 7.2.18+
* [Composer](https://getcomposer.org/) 1.8.5+
* [MariaDB](https://mariadb.org/) 10.1.37+
* [Apache](https://httpd.apache.org/) 2.4+
### Getting Started
When you have a brand new, empty dictionary, the first thing you'll probably want to do is change the title to whatever your conlang is called and add at least a little description of what your language is like or how to use it. You can do this by clicking on the Settings button, which will open up the settings screen. Here, you will find all the fields you need to update your dictionary's Name and Details. The Details text area uses [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) to format any text you include, so brush up on how to do basic things in Markdown before you get started _(NOTE: a line break is done by adding 2 or more spaces to the end of the line and then going to the next line!)_. After this, make sure that the Parts of Speech are adequate for your language _(see below for more information about this)_. Update these fields to what you want them to say and click the "Save" button to keep the Settings menu open, or the "Save and Close" button to close the menu and start adding words!
## Development
To add words, just use the form on the top left side of the window. Hopefully the form is self-explanatory, but if not, here's a little guide:
Enter the word in your language in the "Word" field, the pronunciation of the word in the "Pronunciation" field, choose a Part of Speech, enter an equivalent word/short definition in the "Equivalent Word(s)" field and/or a longer definition or fuller explanation of the word using [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) in the "Explanation/Long Definition" text area, and click "Add Word". Your word will instantly appear in your dictionary under the dictionary's name! You can add as many words as you want this way.
The only things to remember while adding new words is that the minimum information that you can enter is the Word itself and either the Equivalent Word OR the Explanation. You can have both of these or just one, but you need at least one. If you do not want to use the Pronunciation or Parts of Speech then you do not need to, though if you leave out Part of Speech, you will miss out on the handy Filter feature of the dictionary.
`npm start` bundles and watches frontend and backend changes. Set up a junction link to `dist` from the root of your php-processing web server.
And that's all you need to get started! Everything else should be pretty self-explanatory, but a full explanation of Dictionary Builder and all of its functions continues below.
It's less useful, but `npm run serve-frontend-only` will bundle and serve _only_ the front end stuff from `localhost:1234`. The bundled files all still get bundled into `dist`.
### Locking/Unlocking the Word Entry Form
If you are not viewing Lexiconga from a mobile device, you will see a lock icon (🔒) in a button in the top right corner of the word entry form. If you click the button, the word entry form will be undocked from the top of the dictionary and will scroll down the page with you. If you click the unlocked icon (🔓) after the form has been unlocked, it will lock again and jump back to the top of the page.
Parcel Bundler version < 2 doesn't use PostCSS 8+, so upgrading autoprefixer will not work. Keep autoprefixer at the highest version within the 9 major version until you're able to figure out how parcel-bundler 2+ works.
### Viewing your Dictionary's Details
After you enter a markdown-formatted description/rules in the Settings menu, you can view the formatted version by clicking the "Show Description" button under your dictionary's name. You can hide it again by clicking "Hide Description" when the description is displayed.
## Production
### Maximizing Large Text Boxes
If you need more space to see what you are entering into a word's Explanation/Long Definition field or your Dictionary Details, clicking the "Maximize" button above each text area will give you a larger view of the text box to enter text in. When you're done writing, click either the "Minimize" button or any of the darker space outside of the larger view, and your text will be in the right place. 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)!
`npm run bundle` bundles and minifies the frontend stuff and also copies the backend stuff to `dist`. Be sure to run `npm run clear` to delete the contents of `dist` and `.cache` before using `npm run bundle` to make sure you don't get old dev versions of the bundled code included in your upload.
### Entry Management
After adding some words to your dictionary, you'll notice a link icon (🔗) and an "Edit" and "Delete" button attached to each entry.
## Migration
The link icon (🔗) is a link to that word. These links only work when there is nothing entered in the search box and no filters are set. Also note that the links are only intended for linking within the same dictionary and will only work properly when your dictionary is loaded, so only share them with friends if your friends also have your dictionary imported.
There is a script called `src/php/api/migrate.php.changeme` that can be used to help with the migration process from a `version1` Lexiconga database into a `master` database. **Note:** Migration is intended only for migrating from an old server to a freshly-installed/empty new database. To use this, copy `src/php/api/migrate.php.changeme` to `migrate.php` somewhere in the `version1` project (probably in `/php`) and copy the same to `/api/migrate.php` in your `master` project, making sure that all the variables for referencing the databases are correct.
The **Edit** button will scroll to the top of the screen and fill the Word form with the current details of the word you edited. You can make any changes you want and click the "Edit Word" button. You will be asked to confirm your changes, and once you do, your word will be saved. If you do not want to make changes, just click the "Cancel" button.
Visit `migrate.php` on your `version1` server with `?outgoing=true` set in order to begin the transfer. The other server's `migrate.php` will receive an "incoming" request multiple times, and your screen will display messages as it works.
The **Delete** button will ask you to confirm that you want to delete the entry, and if you say yes, the word will be _permanently deleted and **cannot be retrieved**_.
_DELETE THESE `migrate.php` FILES IMMEDIATELY AFTER MIGRATION IS COMPLETE!_.
### The Settings Menu
While you were in the settings menu when you were getting started, you probably noticed some other things you can set there, like "Parts of Speech", "Allow Duplicates", "Case-Sensitive", and "Dictionary is complete", not to mention the export, import, and erase buttons.
## Emails
The **Parts of Speech** field is where you can add custom parts of speech for your language if you need to! Just list your parts of speech in a comma-separated list the same way as the default parts of speech are listed, and your options in the word form and filters will update as soon as you save!
>_Please note that if you have other parts of speech added to existing words, those words will not update and will keep the old parts of speech. You will need to manually update any words with incorrect parts of speech after the fact, which is why I recommend you update the available parts of speech as one of the first things you do if you need to change them at all!_
Be sure you set up email senders/receivers for at least these 3 email addresses:
The **Allow Duplicates** checkbox allows you to control whether or not Dictionary Builder will allow you to add the same word multiple times. If you leave Allow Duplicates unchecked and you try to add a word that is already in the dictionary, Dictionary Builder will tell you that the word already exists and will ask if you want to update it with the newly entered word.
The **Case-Sensitive** checkbox allows you to control Dictionary Builder's duplicate detection. If you leave Case-Sensitive unchecked, you will be alerted when you are trying to add a word with the same letters to your dictionary a second time. For example, "dog" is identified as the same word as "DOG" or "doG". The dictionary will keep whatever capitalization you save but it will identify words with the same spelling as duplicates. If Case-Sensitive is checked, then it will not identify "dog" and "DOG" as the same word.
If Allow Duplicates is checked, this checkbox becomes unavailable.
The **Dictionary is Complete** checkbox will make the word add/edit form go away so you can view or share it more easily/safely. Plus when you export your dictionary, all of the options to change anything about your dictionary will be excluded when it is re-imported! Your dictionary will become static, and will not be able to be changed or updated without a password.
The **Total Entries** label is just a live tally of how many words you have added to the current dictionary.
The **Export Current Dictionary** button will immediately do exactly that. Your browser will start downloading a file with your dictionary's name in a ".dict" format. Please note that this may not work as expected on mobile platforms. This export can be a personal backup for your own uses, to work on multiple dictionaries at a time (i.e. export one dictionary and import the other to work on the one you'd like), or you can share it with friends to view it.
The **Import Dictionary** form allows you to upload and view any previously-exported ".dict" files. After selecting your ".dict" file, click the "Import" button to _overwrite your current dictionary_ and view the imported one. Again, please note that this import process will _**permanently overwrite your current dictionary**_, so please be sure to export your dictionary _before_ you import a new one.
The **Empty Current Dictionary** should only be used if you want to completely start over from scratch. It will ask you to confirm that you want to delete, and if you confirm, your dictionary will be gone forever. If you have not exported your dictionary before emptying it, there will be absolutely no way to get it back. Please be careful with this!
### Search/Filter
You can search entries or filter by part of speech by clicking the "Search/Filter Options" button to expand the search panel.
From there, you can enter any text you want in the search box and either press Enter or click anywhere outside the search box, and Lexiconga will display any and every entry including your entry. To display the entire dictionary again, you must clear the search box.
You can refine your search by using the checkboxes below the search box:
* **Word**: When checked, Lexiconga searches your dictionary's "Word" entries for the entered text. When unchecked, it ignores it.
* **Equivalent**: When checked, Lexiconga searches your dictionary's "Equivalent Word(s)" entries for the entered text. When unchecked, it ignores it.
* **Explanation**: When checked, Lexiconga searches your dictionary's "Explanation/Long Definition" entries for the entered text. When unchecked, it ignores it.
* **Search Case-Sensitive**: When checked, Lexiconga finds entries matching the letter case in the entered text. When unchecked, it will find any case as long as the letters match.
* **Ignore Diacritics/Accents**: When checked, Lexiconga will ignore accented letters and diacritics and identify them as their equivalent unaccented letter and vice-versa, in case you want to find a word with a diacritic without entering the diacritic in the search box. When unchecked, it will only find diacritics and accented letters if they are specifically entered in the search box.
The "Filter Words" drop-down box allows you to filter your dictionary by part of speech. To display the whole dictionary again after setting a filter, reset the filter option to "All".
### Keyboard Shortcuts
**Esc** : Exits a window (i.e. Dictionary Settings, Account Settings, this about page, etc.) without saving.
**Ctrl/Control +**
* **Enter/Return** : Submit Word (when typing in Word Form)
* **D** : Toggle Dictionary Description visibility.
* **E** : Export current dictionary. (Also Ctrl+Shift+S does this.)
* **H** : Open this help window.
* **M** : Maximize/Minimize Full Screen textbox when typing in the boxes that have the Maximize button.
* **S** : Jump to Search box.
* **U** : Toggle Word Form lock.
**Alt/Option +**
* **A** : Toggle Account Settings window (if logged in).
* **S** : Toggle Dicitonary Settings window. Saves & Closes if it's already open.
## 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.
### Creating An Account
The first time you create an account, you will need to enter your email address and a password (for logging in) in addition to a "Public Name". Your Public Name will be more important when we add dictionary sharing later, but for now, it is important in that it helps indicate whether or not you are logged in (see below). If you have a dictionary loaded in your browser, it will be automatically uploaded to your account and saved after it is created.
### Logging In
To log in after creating an account, just click the "Log In/Create Account" button and enter your email address and password under the "Log In" form, just like any other account online. You will know that you are logged in from the "Welcome back!" notification at the top of the screen when you load the page. You can also know that you're logged in if you see a "Log Out" button instead of "Log In/Create Account" in the top right corner of the screen.
### Differences
Every time you save a change to your dictionary's settings or add, edit, or delete a word, the changes are automatically saved to both your browser's localStorage in addition to being sent to your account. If you're paranoid that your changes are not being saved, you can check your browser's console log to see the little save and update notifications.
#### Account Settings
After logging in, you'll see an "Account Settings" button in the top, right side of the Lexiconga window. Clicking this will allow you to change a few settings about your account:
The **Email** field allows you to specify a different login and contact email address. Make sure that you do not forget what you chose, because there is no way to retrieve your email address if you change it to something you forget!
The **Public Name** field allows you to change your public name.
The **Allow Emails** checkbox allows you to choose if you would like to receive emails about important Lexiconga updates. Make sure that you allow emails from addresses at lexicon.ga or check your spam folder just in case. Note that this checkbox does not affect password reset requests—if you forget your password, Lexiconga will send you a password reset email regardless of your choice here.
If you change any of the three options above, be sure you click the "Save Settings" button.
The "Reset Password" button in the "Reset Your Password" section will allow you to reset your login password. Don't forget it!
#### Dictionary Settings
Under the Settings menu, you'll see some additional options:
The **Dictionary is Public** checkbox determines whether or not the current dictionary can be viewed by anyone online using the Public Link that appears when checked. Public dictionaries are explained more below.
The **Change Dicitonaries** dropdown box contains the names of all of your created dictionaries. If you have more than one, selecting a different dictionary from the list will immediately download and display that dictionary.
The **Create New Dictionary** button will instantly create and save a new blank dictionary to your account.
The **Import Dictionary** button acts the same as before, but instead of overwriting your dictionary, it imports the dictionary as a new, separate dictionary and saves it to your account. After importing, the imported dictionary will display, and you can use the Change Dictionaries dropdown box to change to a previous one if you desire.
The **Delete Current Dictionary** button will permanently and irretrievably delete the currently loaded dictionary from your account! Be careful with that one. After deleting, you will then be prompted to either select another dictionary to load or create a new one, _or_ if you have no other dictionaries, immediately create a new one for you.
#### Public Dictionaries
When a dictionary is marked as public, you can share its public link and allow anyone to view its contents without being able to make changes. The dictionary's description and the search/filter area is visible by default, and the viewer can scroll through or search your dictionary without being able to make changes.
To log in or create an account when viewing a dictionary, you need to go back to the main Lexiconga page. You can get there by clicking either the logo or the "Go Home" button.
### Forgot Your Password?
If you forget your password, you can request a password reset email by clicking the "Forgot Password" button on the "Log In/Create Account" button entering the email address associated with your account and clicking "Email Password Reset Key". This will send an email (_check your spam_) with a link that will allow you to reset your password. When you go to the link provided, you'll be able to enter a new password that you can log in with.
### Lockout
If you manage to enter your password wrong 10 times, you'll be locked out from logging in for an hour. Use this time to try to remember your password or something. You can get an idea of how long you've waited by refreshing the page and clicking the unfortunate "Can't Login" button. After an hour has passed, refresh the page again and you'll get another 10 tries.
## Problems or Requests
Please report any problems you come across to the [Dictionary Builder Issues page](https://github.com/Alamantus/DictionaryBuilder/issues). You can also submit enhancement requests to the same place if you have any requests for new features.
## Update Log
You can see all previous updates to Lexiconga here:
[https://github.com/Alamantus/DictionaryBuilder/releases](https://github.com/Alamantus/DictionaryBuilder/releases/tag/Production)
## Future Plans
In the future, I'm planning to:
* enable account deletion if you lose trust or hope in Lexiconga's services
* provide an ad removal option?
## Thanks!
If you like Lexiconga and want to buy me a cup of coffee for the service, you can **[donate throughPaypal](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=MCCSYGQCR5TLY&lc=US&item_name=Lexiconga&currency_code=USD&bn=PP%2dDonationsBF%3abtn_donate_SM%2egif%3aNonHosted)** to help keep it online if you want.
I hope you enjoy Lexiconga and that it helps you build some awesome languages.
Robbie Antenesse
## Libraries Used
* [Marked.js](https://github.com/chjj/marked) by Christopher Jeffrey (JJ) (a.k.a. chjj)
* [Defiant.js](http://defiantjs.com) by Hakan Bilgin (a.k.a. hbi99)
* [removeDiacritics.js](http://stackoverflow.com/a/18391901/3508346) by [rdllopes](http://meta.stackoverflow.com/users/1879686/rdllopes)
- help (can be forwarder)
- donotreply (must be sender)

View File

@ -1 +0,0 @@
The Filter system has been updated—you can now filter on as many parts of speech as you want!

9
composer.json Normal file
View File

@ -0,0 +1,9 @@
{
"config": {
"vendor-dir": "src/php/vendor"
},
"require": {
"firebase/php-jwt": "^5.0",
"hashids/hashids": "^4.0"
}
}

131
composer.lock generated Normal file
View File

@ -0,0 +1,131 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "b2cfd40da6d3f90b0110c83afb8c3c0e",
"packages": [
{
"name": "firebase/php-jwt",
"version": "v5.0.0",
"source": {
"type": "git",
"url": "https://github.com/firebase/php-jwt.git",
"reference": "9984a4d3a32ae7673d6971ea00bae9d0a1abba0e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/9984a4d3a32ae7673d6971ea00bae9d0a1abba0e",
"reference": "9984a4d3a32ae7673d6971ea00bae9d0a1abba0e",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": " 4.8.35"
},
"type": "library",
"autoload": {
"psr-4": {
"Firebase\\JWT\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Neuman Vong",
"email": "neuman+pear@twilio.com",
"role": "Developer"
},
{
"name": "Anant Narayanan",
"email": "anant@php.net",
"role": "Developer"
}
],
"description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
"homepage": "https://github.com/firebase/php-jwt",
"time": "2017-06-27T22:17:23+00:00"
},
{
"name": "hashids/hashids",
"version": "4.0.0",
"source": {
"type": "git",
"url": "https://github.com/ivanakimov/hashids.php.git",
"reference": "43bb2407f16a631f0128f47bcb67ff986c63dde2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ivanakimov/hashids.php/zipball/43bb2407f16a631f0128f47bcb67ff986c63dde2",
"reference": "43bb2407f16a631f0128f47bcb67ff986c63dde2",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^7.2"
},
"require-dev": {
"phpunit/phpunit": "^8.0"
},
"suggest": {
"ext-bcmath": "Required to use BC Math arbitrary precision mathematics (*).",
"ext-gmp": "Required to use GNU multiple precision mathematics (*)."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.0-dev"
}
},
"autoload": {
"psr-4": {
"Hashids\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ivan Akimov",
"email": "ivan@barreleye.com",
"homepage": "https://twitter.com/IvanAkimov"
},
{
"name": "Vincent Klaiber",
"email": "hello@doubledip.se",
"homepage": "https://doubledip.se"
}
],
"description": "Generate short, unique, non-sequential ids (like YouTube and Bitly) from numbers",
"homepage": "http://hashids.org/php",
"keywords": [
"bitly",
"decode",
"encode",
"hash",
"hashid",
"hashids",
"ids",
"obfuscate",
"youtube"
],
"time": "2019-04-03T13:40:29+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": []
}

View File

@ -1,154 +0,0 @@
body {
background: #e6cfaa;
}
header {
height: 50px;
width: 100%;
background: #eacc9d;
margin-bottom: 3px;
border-bottom: 3px solid #e3bb7d;
}
/* Smartphones (portrait and landscape) ----------- */
@media only screen
and (min-device-width : 320px)
and (max-device-width : 480px) {
header {
height: 70px;
}
}
tr, thead {
border-bottom: 1px solid #af8050;
}
td, th {
border-right: 1px solid #af8050;
}
a {
text-decoration: underline;
color: #a01000;
}
#siteLogo {
display: block;
text-indent: -9999px;
width: 242px;
height: 48px;
background: url(../images/logo.svg);
background-size: 242px 48px;
float: left;
}
#aboutButton {
background: #dcb078;
}
#loginoutArea {
float: right;
}
#loginoutArea a {
color: #000000;
text-decoration: none;
font-size: 13px;
}
@media screen
and (min-device-width : 481px) {
#headerPadder {
padding: 1px;
}
#loginoutArea {
margin: 16px;
}
}
/* Smartphones (portrait and landscape) ----------- */
@media only screen
and (min-device-width : 320px)
and (max-device-width : 480px) {
#siteLogo {
display: block;
text-indent: -9999px;
width: 150px;
height: 30px;
background: url(../images/logo.svg);
background-size: 150px 30px;
float: left;
}
#loginoutArea {
margin: 16px 8px;
}
}
#wordEntryForm {
background: #c05d5d;
border-color: #c08d8d #b00d0d #b00d0d #c08d8d;
}
input, textarea, select, option, button {
background: #efdfc0;
}
#announcementArea {
background:#a0c066;
}
#notificationArea {
background:#c0c088;
}
#dictionaryContainer {
background: #bd7251;
padding: 15px;
border: outset;
border-color: #d09b84 #915237 #915237 #d09b84;
}
#dictionaryDescription, .management, #settingsOptions, #infoPage, #loadAfterDeletePage,
#accountSettingsPage, #fullScreenTextboxPage {
background: #f2d5b2;
}
.management {
border-width: 2px;
border-color: #ebbe87 #f9ecdd #f9ecdd #ebbe87;
}
#dictionaryName {
text-shadow: 2px 2px 2px #915337;
}
#dictionaryDescription {
width: 90%;
}
#loginLink, #logoutLink,
#descriptionToggle, #searchFilterToggle, #settingsButton,
.deleteCancelButton, .deleteConfirmButton,
#settingsScreenCloseButton, #infoScreenCloseButton,
.clickable, .helperlink {
background: #e0c19c;
}
entry {
background: #d7ad7d;
border-color: #e7cfb3 #c78b47 #c78b47 #e7cfb3;
}
.editButton {
background: #89cb89;
}
.deleteButton {
background: #cc8888;
}
footer {
background: #d69c42;
border-top: 3px solid #e3bb7d;
}

View File

@ -1,439 +0,0 @@
body {
padding: 0;
margin: 0;
border: none;
font-family: Georgia, 'Times New Roman', Times, serif;
}
contents {
display: block;
width: 100%;
}
footer {
width: 100%;
text-align: center;
vertical-align: middle;
position:fixed;
bottom: 0px;
left: 0px;
background: #aaaaaa;
padding: 6px;
max-height: 32px; /* Update Dictionary Container's bottom margin to account for footer */
}
table {
border-collapse: collapse;
width: 100%;
}
tr, thead {
border-bottom: 1px solid #afafaf;
}
tr:last-child {
border-bottom: none;
}
td, th {
border-right: 1px solid #afafaf;
padding: 3px 10px;
}
td:last-child, th:last-child {
border-right: none;
}
.inline {
display: inline !important;
}
#leftColumn {
float: left;
}
form {
padding: 15px;
}
#wordEntryForm {
width: 30%;
max-width: 400px;
min-width: 260px;
margin: 15px 0 15px 15px;
border: outset 3px;
}
label {
display: block;
margin-bottom: 10px;
}
label span {
display: block;
font-weight: bold;
}
label span .helperlink {
font-size: 10px;
}
label label {
margin-left: 20px;
}
label span.checkboxlabel {
display: inline;
margin-left: 10px;
}
input, textarea {
display: block;
padding-left: 5px;
}
input[type=checkbox] {
display: inline;
margin: 5px;
}
#longDefinition {
width: 80%;
min-width: 260px;
height: 150px;
}
#updateConflict {
width: 260px;
}
#errorMessage, #updateConflictMessage, #settingsErrorMessage {
display: block;
color: maroon;
font-weight: bold;
}
#dictionaryWarn {
margin-top: 10px;
font-size: 12px;
font-weight: bold;
font-style: italic;
}
.exportWarnText {
display: inline;
color: red;
cursor: pointer;
text-decoration: underline;
}
#formLockButton {
float: right;
}
#createAccountSubmitButton, #accountSettingsSubmitButton {
margin-top: 10px;
}
#aboutButton {
display: inline;
margin: 0 10px 0 0;
}
#announcementArea, #notificationArea {
text-align:center;
padding:10px;
border-radius:5px;
margin:0 auto;
width:50%;
min-width:200px;
}
#dictionaryContainer {
margin: 15px 0 36px 15px; /* bottom margin must clear footer */
width: 60%;
min-width: 260px;
max-width: 600px;
float: left;
}
#dictionaryName {
margin: 0 0 5px;
}
#dictionaryDescription {
width: 100%;
max-height: 400px;
overflow-y: auto;
padding: 15px;
border: 1px solid #cacaca;
margin: 10px;
}
.clickable, .helperlink {
display: inline;
font-weight: bold;
font-size: 13px;
padding: 4px;
background: #dddddd;
border-radius: 5px;
}
.clickable {
cursor: pointer;
}
#descriptionToggle, #searchFilterToggle {
display: inline-block;
margin: 8px;
font-weight: bold;
font-size: 12px;
cursor: pointer;
}
.searchOption, .filterOption {
font-size:12px;
display: inline-block;
margin: 0 8px 0 0;
}
#wordFilter {
margin: 10px 0;
}
#filterWordCount {
margin: 10px 10px;
display: block;
font-weight: bold;
font-style: italic;
}
entry {
display: block;
width: 90%;
min-width: 200px;
padding: 10px 10px 3px;
border: outset 3px;
margin-bottom: 5px;
}
.wordLink {
text-decoration: none;
float: right;
font-size: 13px;
padding: 2px;
line-height: 10px;
}
word {
font-weight: bold;
font-size: 20px;
}
pronunciation {
font-size: 12px;
margin-left:10px;
}
partofspeech {
font-style: italic;
font-weight: bold;
font-size: 10px;
margin-left:10px;
}
simpledefinition {
display: block;
font-style: italic;
}
longdefinition {
display: block;
margin-left: 20px;
}
longDefinition h1, longDefinition h2, longDefinition h3, longDefinition h4, longDefinition h5, longDefinition h6 {
margin: 5px 0 8px;
font-weight: bold;
}
longDefinition h1 {
font-size: 22px;
}
longDefinition h2 {
font-size: 20px;
}
longDefinition h3 {
font-size: 20px;
font-weight: normal;
}
longDefinition h4 {
font-size: 18px;
}
longDefinition h5 {
font-size: 18px;
font-weight: normal;
}
longDefinition h6 {
font-size: 17px;
}
longDefinition p {
margin: 3px 0 8px;
}
searchTerm {
display: inline;
color: #ff0000;
background: #ffff00;
padding: 1px;
text-decoration: underline;
font-style: italic;
font-weight: bold;
}
.management {
display: block;
right: 5px;
width: 100px;
padding: 3px;
border: inset 3px;
margin: 10px;
}
.editButton, .deleteButton, .deleteConfirmButton, .deleteCancelButton {
display: inline;
font-size: 10px;
margin: 5px;
}
.deleteConfirm {
display: block;
font-size: 10px;
margin: 10px;
}
#settingsBackgroundFade, #infoBackgroundFade, #loadAfterDeleteFade,
#accountSettingsBackgroundFade, #fullScreenTextboxBackgroundFade {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #aaaaaa;
opacity: 0.75;
}
#settingsOptions, #infoPage, #loadAfterDeletePage,
#accountSettingsPage, #fullScreenTextboxPage {
position: fixed;
top: 6%;
left: 6%;
right: 6%;
bottom: 6%;
min-width: 260px;
min-height: 260px;
padding: 5px 5% 5%;
overflow-y: auto;
overflow-x: hidden;
background: #ffffff;
opacity: 1;
border-radius: 5px;
border: 1px solid black;
}
#announcementCloseButton, #notificationCloseButton,
#settingsButton, #settingsScreenCloseButton,
#settingsSaveButtons button, #infoScreenCloseButton,
#fullScreenTextboxScreenCloseButton {
float: right;
font-size: 12px;
}
#fullScreenTextboxPage {
padding: 5px 3% 4%;
overflow-y: hidden;
}
#fullScreenTextbox {
position: relative;
width: 100%;
height: 100%;
}
#settingsForm {
width: 100%;
padding: 0;
margin: 0;
}
#publicLink {
font-size:12px;
font-style: italic;
}
#dictionaryDescriptionEdit, #dictionaryPartsOfSpeechEdit {
width: 100%;
max-width: 360px;
min-width: 200px;
}
#dictionaryDescriptionEdit {
height: 200px;
}
#settingsErrorMessage {
float: right;
clear: both;
}
#settingsSaveButtons {
position: fixed;
right: 10%;
bottom: 8%;
font-size: 14px;
display: block;
width: 50%;
}
.settingsCol {
display: block;
float: left;
width: 30%;
min-width: 260px;
max-width: 400px;
margin: 0 30px 0 0;
}
/* Smartphones (portrait and landscape) ----------- */
@media only screen
and (min-device-width : 320px)
and (max-device-width : 480px) {
footer {
position: relative;
clear: both;
max-height: 100%;
}
#dictionaryContainer {
margin-bottom: 15px !important;
}
#formLockButton {
display: none;
}
#showFullScreenTextbox {
display: none;
}
#settingsSaveButtons {
position: relative;
right: 0;
bottom: 0;
clear: both;
width: 100%;
}
}

50
dev/resize-images.js Normal file
View File

@ -0,0 +1,50 @@
const fs = require('fs');
const sharp = require('sharp');
const folder = './processedImages/';
if (!fs.existsSync(folder)) {
fs.mkdirSync(folder);
}
const favicon = sharp('./src/images/favicon.svg');
sharp('./src/images/social.jpg').toFile(folder + 'social.jpg', (err, info) => {
if (err) return console.error(err);
console.log(info);
});
favicon.clone().resize(32, 32).toFile(folder + 'favicon.png', (err, info) => {
if (err) return console.error(err);
console.log(info);
});
favicon.clone().resize(128, 128).toFile(folder + 'icon-128.png', (err, info) => {
if (err) return console.error(err);
console.log(info);
});
favicon.clone().resize(144, 144).toFile(folder + 'icon-144.png', (err, info) => {
if (err) return console.error(err);
console.log(info);
});
favicon.clone().resize(152, 152).toFile(folder + 'icon-152.png', (err, info) => {
if (err) return console.error(err);
console.log(info);
});
favicon.clone().resize(192, 192).toFile(folder + 'icon-192.png', (err, info) => {
if (err) return console.error(err);
console.log(info);
});
favicon.clone().resize(256, 256).toFile(folder + 'icon-256.png', (err, info) => {
if (err) return console.error(err);
console.log(info);
});
favicon.clone().resize(512, 512).toFile(folder + 'icon-512.png', (err, info) => {
if (err) return console.error(err);
console.log(info);
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 36 KiB

300
index.php
View File

@ -1,300 +0,0 @@
<?php
require_once('required.php');
session_start();
$current_user = isset($_SESSION['user']) ? $_SESSION['user'] : 0;
$announcement = get_include_contents(SITE_LOCATION . '/announcement.php');
$notificationMessage = "";
if ($current_user > 0 || !isset($_SESSION['loginfailures']) || (isset($_SESSION['loginlockouttime']) && time() - $_SESSION['loginlockouttime'] >= 3600)) {
// If logged in, never failed, or more than 1 hour has passed, reset login failures.
$_SESSION['loginfailures'] = 0;
} else {
$alertlockoutmessage = "You failed logging in 10 times. To prevent request flooding and hacking attempts, you may not log in or create an account for 1 hour.\\n\\nThe last time this page was loaded, you had been locked out for " . time_elapsed(time() - $_SESSION['loginlockouttime']) . "\\n\\nRefresh the page once the hour has passed.";
$hoverlockoutmessage = str_replace("\\n", "\n", $alertlockoutmessage);
}
require_once(SITE_LOCATION . '/php/notificationconditiontree.php');
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lexiconga Dictionary Builder</title>
<link href="css/styles.css" rel="stylesheet" />
<link href="css/lexiconga.css" rel="stylesheet" />
</head>
<body>
<header>
<div id="headerPadder">
<a href="/" id="siteLogo">Lexiconga Dictionary Builder</a>
<div style="float:right;margin: 16px 8px;font-size:12px;">
<span id="aboutButton" class="clickable" onclick="ShowInfo('aboutText')">About Lexiconga</span>
</div>
<div id="loginoutArea" style="font-size:12px;">
<?php if ($current_user > 0) { //If logged in, show the log out button. ?>
<span id="accountSettings" class="clickable" onclick="ShowAccountSettings()">Account Settings</span> <a href="?logout" id="logoutLink" class="clickable">Log Out</a>
<?php } elseif (!isset($_SESSION['loginfailures']) || (isset($_SESSION['loginfailures']) && $_SESSION['loginfailures'] < 10)) { ?>
<span id="loginLink" class="clickable" onclick="ShowInfo('loginForm')">Log In/Create Account</span>
<?php } else { ?>
<span id="loginLink" class="clickable" title="<?php echo $hoverlockoutmessage; ?>" onclick="alert('<?php echo $alertlockoutmessage; ?>');">Can't Login</span>
<?php } ?>
</div>
</div>
</header>
<contents>
<div id="announcementArea" style="display:<?php echo (($announcement) ? "block" : "none"); ?>;margin-bottom:10px;">
<span id="announcementCloseButton" class="clickable" onclick="document.getElementById('announcementArea').style.display='none';">Close</span>
<div id="announcement"><?php echo $announcement; ?></div>
</div>
<div id="notificationArea" style="display:<?php echo (($notificationMessage) ? "block" : "none"); ?>;">
<span id="notificationCloseButton" class="clickable" onclick="document.getElementById('notificationArea').style.display='none';">Close</span>
<div id="notificationMessage"><?php echo $notificationMessage; ?></div>
</div>
<div id="leftColumn">
<form id="wordEntryForm">
<div id="formLockButton" class="clickable" onclick="ToggleWordFormLock()" title="Lock/unlock form from the top of the page">&#128274;</div>
<label><span>Word</span>
<input type="text" id="word" onkeydown="SubmitWordOnCtrlEnter(this)" />
</label>
<label><span>Pronunciation <a class="helperlink" href="/ipa_character_picker/" target="_blank" title="IPA Character Picker backed up from http://r12a.github.io/pickers/ipa/">IPA Characters</a></span>
<input type="text" id="pronunciation" onkeydown="SubmitWordOnCtrlEnter(this)" />
</label>
<label><span>Part of Speech</span>
<select id="partOfSpeech" onkeydown="SubmitWordOnCtrlEnter(this)"></select>
</label>
<label><span>Equivalent Word(s)</span>
<input type="text" id="simpleDefinition" onkeydown="SubmitWordOnCtrlEnter(this)" />
</label>
<label><span>Explanation/Long Definition <span id="showFullScreenTextbox" class="clickable" onclick="ShowFullScreenTextbox('longDefinition', 'Explanation/Long Definition')">Maximize</span></span>
<textarea id="longDefinition" onkeydown="SubmitWordOnCtrlEnter(this)"></textarea>
</label>
<input type="hidden" id="editIndex" />
<span id="errorMessage"></span>
<div id="newWordButtonArea" style="display: block;">
<button type="button" onclick="AddWord(); return false;">Add Word</button>
</div>
<div id="editWordButtonArea" style="display: none;">
<button type="button" onclick="AddWord(); return false;">Edit Word</button> <button type="button" onclick="ClearForm(); window.scroll(savedScroll.x, savedScroll.y); return false;">Cancel</button>
</div>
<div id="updateConflict" style="display: none;"></div>
</form>
</div>
<div id="dictionaryContainer">
<span id="settingsButton" class="clickable" onclick="ShowSettings()">Settings</span>
<h1 id="dictionaryName"></h1>
<span id="descriptionToggle" class="clickable" onclick="ToggleDescription();">Show Description</span>
<div id="dictionaryDescription" style="display:none;"></div>
<span id="searchFilterToggle" class="clickable" onclick="ToggleSearchFilter();">Search/Filter Options</span>
<div id="searchFilterArea" style="display:none;">
<div id="searchArea" style="display:block;">
<label style="margin-top:10px;">
<span>Search</span>
<div style="display:block;">
<input type="text" id="searchBox" onclick="this.select();" onchange="ShowDictionary()" style="display:inline;" />&nbsp;
<span style="display:inline;cursor:pointer;font-size:10px;font-weight:bold;" onclick="document.getElementById('searchBox').value='';ShowDictionary();">Clear Search</span>
</div>
<div id="searchOptions">
<label class="searchOption">Word <input type="checkbox" id="searchOptionWord" checked="checked" onchange="ShowDictionary()" /></label>
<label class="searchOption">Equivalent <input type="checkbox" id="searchOptionSimple" checked="checked" onchange="ShowDictionary()" /></label>
<label class="searchOption">Explanation <input type="checkbox" id="searchOptionLong" checked="checked" onchange="ShowDictionary()" /></label>
<br />
<label class="searchOption">Search Case-Sensitive <input type="checkbox" id="searchCaseSensitive" onchange="ShowDictionary()" /></label>
<label class="searchOption" title="Note: Matching diacritics will appear but may not highlight.">Ignore Diacritics/Accents <input type="checkbox" id="searchIgnoreDiacritics" onchange="ShowDictionary()" /></label>
</div>
</label>
</div>
<label style="display:block;margin-bottom:0;"><b>Filter Words</b></label>
<div id="filterOptions" style="display:block"></div>
<div style="display:block;">
<span style="display:inline;cursor:pointer;font-size:12px;font-weight:bold;" onclick="ToggleAllFilters(true)">Check All</span>&nbsp;/&nbsp;<span style="display:inline;cursor:pointer;font-size:12px;font-weight:bold;" onclick="ToggleAllFilters(false)">Uncheck All</span>
</div>
</div>
<div id="filterWordCount"></div>
<div id="theDictionary"></div>
</div>
<div id="rightColumn" class="googleads" style="float:right;width:20%;max-width:300px;min-width:200px;overflow:hidden;">
<?php if ($_GET['adminoverride'] != "noadsortracking") { include_once("php/google/adsense.php"); } ?>
</div>
<div id="settingsScreen" style="display:none;">
<div id="settingsBackgroundFade" onclick="HideSettings()"></div>
<div id="settingsOptions">
<span id="settingsScreenCloseButton" class="clickable" onclick="HideSettings()">Close</span>
<h2>Dictionary Settings</h2>
<form id="settingsForm">
<div class="settingsCol">
<div id="hideIfComplete">
<label>
<span>Dictionary Name</span>
<input type="text" id="dictionaryNameEdit" />
</label>
<label><span>Dictionary Details <span id="showFullScreenTextbox" class="clickable" onclick="ShowFullScreenTextbox('dictionaryDescriptionEdit', 'Dictionary Details')">Maximize</span></span>
<textarea id="dictionaryDescriptionEdit"></textarea>
</label>
<label>
<span>Parts of Speech</span>
<input type="text" id="dictionaryPartsOfSpeechEdit" />
</label>
<label>
<span class="checkboxlabel">Allow Duplicates</span>
<input type="checkbox" id="dictionaryAllowDuplicates" onchange="ToggleCaseSensitiveOption()" />
<label>
<span class="checkboxlabel">Case-Sensitive</span>
<input type="checkbox" id="dictionaryCaseSensitive" />
</label>
</label>
<label class="inline">
<span class="checkboxlabel">Sort by Equivalent Word</span>
<input type="checkbox" id="dictionarySortByEquivalent" />
</label> <span class="helperlink clickable" onclick='alert("By default, your dictionary is organized alphabetically by word. Checking this box will organize it by the \"Equivalent Word\" field instead");'>?</span>
</div>
<br>
<label>
<span class="checkboxlabel">Dictionary is Complete</span>
<input type="checkbox" id="dictionaryIsComplete" />
</label>
<?php if ($current_user > 0) { //If logged in, show the log out button. ?>
<label class="inline">
<span class="checkboxlabel">Dictionary is Public</span>
<input type="checkbox" id="dictionaryIsPublic" onchange="TogglePublicLink()" />
</label> <span class="helperlink clickable" onclick='alert("If you save your settings with this checked, your dictionary will be viewable by anyone if they have the public link.");'>?</span>
<div id="publicLink"></div>
<?php } ?>
</div>
<div class="settingsCol">
<label>
<b>Total Entries:</b> <i id="numberOfWordsInDictionary"></i>
</label>
<label><button type="button" onclick="ExportDictionary()" style="cursor:pointer;">Export Current Dictionary</button></label>
<?php if ($current_user > 0) { //If logged in, show the special options. ?>
<label><span>Change Dictionaries</span>
<select id="userDictionaries" onchange="ChangeDictionary();"></select>
</label>
<label><button type="button" onclick="CreateNewDictionary()" style="cursor:pointer;">Create a New Dictionary</button></label>
<?php } ?>
<label>
<span>Import Dictionary</span>
<input type="file" id="importFile" />
<button type="button" onclick="ImportDictionary(); return false;">Import</button>
</label>
<?php if ($current_user > 0) { //If logged in, show the log out button. ?>
<label><button type="button" onclick="DeleteCurrentDictionary()" style="cursor:pointer;">Delete Current Dictionary</button></label>
<?php } else { //If logged in, show the log out button. ?>
<label><button type="button" onclick="EmptyWholeDictionary()" style="cursor:pointer;">Empty Current Dictionary</button></label>
<?php } ?>
</div>
<div id="settingsSaveButtons">
<span id="settingsErrorMessage"></span><br>
<button type="button" onclick="SaveSettings(); HideSettings(); return false;">Save and Close</button>
<button type="button" onclick="SaveSettings(); return false;">Save</button>
</div>
</form>
</div>
</div>
<div id="fullScreenTextboxScreen" style="display:none;">
<div id="fullScreenTextboxBackgroundFade" onclick="HideFullScreenTextbox()"></div>
<div id="expandedTextboxId" style="display:none;width:0px;height:0px;"></div>
<div id="fullScreenTextboxPage">
<span id="fullScreenTextboxScreenCloseButton" class="clickable" onclick="HideFullScreenTextbox()">Minimize</span>
<label><span id="fullScreenTextboxLabel"></span></label>
<textarea id="fullScreenTextbox"></textarea>
</div>
</div>
<div id="infoScreen" style="display:none;">
<div id="infoBackgroundFade" onclick="HideInfo()"></div>
<div id="infoPage">
<span id="infoScreenCloseButton" class="clickable" onclick="HideInfo()">Close</span>
<div id="infoText"></div>
</div>
</div>
<?php if ($current_user > 0) {
$user_email = Get_User_Email($current_user);
?>
<div id="accountSettingsScreen" style="display:none;">
<div id="accountSettingsBackgroundFade" onclick="HideAccountSettings()"></div>
<div id="accountSettingsPage">
<span id="accountSettingsScreenCloseButton" class="clickable" onclick="HideAccountSettings()">Close</span>
<div class="settingsCol"><form id="accountSettingsForm" method="post" action="?accountsettings">
<h2>Account Settings</h2>
<label><span>Email</span>
<input type="email" id="accountSettingsEmailField" name="email" value="<?php echo $user_email; ?>" onchange="WarnEmailChange()" />
<input type="hidden" id="accountSettingsPreviousEmailField" name="previousemail" value="<?php echo $user_email; ?>" />
</label>
<div id="accountSettingsEmailChangeWarning" style="display:none;font-weight:bold;color:#dd5500;font-size:11px;margin-bottom:10px;">If you change your email address, please note that you will no longer be able to log in with your old email address, <?php echo $user_email; ?>.<br>Change it back unless you are completely sure that you want to change your email address!</div>
<label><span>Public Name <span class="clickable" onclick="ExplainPublicName()" style="font-size:11px;vertical-align:top;background:#e0c19c;padding:4px 7px;">?</span></span>
<input type="text" id="accountSettingsPublicNameField" name="publicname" value="<?php echo Get_Public_Name_By_Id($current_user); ?>" />
</label>
<label style="display:inline;"><b>Allow Emails</b>
<input type="checkbox" id="createAccountAllowEmailsField" name="allowemails" checked="checked" />
</label> <span class="clickable" onclick="ExplainAllowEmails()" style="font-size:11px;vertical-align:top;background:#e0c19c;padding:4px 7px;">?</span>
<div id="accountSettingsError" style="font-weight:bold;color:red;"></div>
<button type="submit" id="accountSettingsSubmitButton" onclick="ValidateAccountSettings(); return false;">Save Settings</button>
<br><br>
<h2>Reset Your Password</h2>
<p style="font-size: 12px;">Click the button below to reload the page and show the Reset Password form. Filling out this form will instantly change your password, and you will need to log in using the new password from that point forward.</p>
<span id="resetPassword" class="clickable" onclick="this.innerHTML='Loading...';LoggedInResetPassword();" style="margin-top:20px;">Reset Password</span>
</form></div>
</div>
</div>
<?php
}
?>
<div id="loadAfterDeleteScreen" style="display:none;">
<div id="loadAfterDeleteFade"></div>
<div id="loadAfterDeletePage">
<div class="settingsCol">
<h1>Dictionary Deleted</h1>
<label>Select dictionary to load:<br />
<select id="loadAfterDelete" onchange="ChangeDictionary(this);document.getElementById('loadAfterDeleteScreen').style.display = 'none';"></select>
</label>
<p>Or</p>
<label><button type="button" onclick="CreateNewDictionary();document.getElementById('loadAfterDeleteScreen').style.display = 'none';" style="cursor:pointer;">Create a New Dictionary</button></label>
</div>
</div>
</div>
</contents>
<footer>
Dictionary Builder only guaranteed to work with most up-to-date HTML5 browsers. <a href="https://github.com/Alamantus/DictionaryBuilder/issues" target="_blank">Report a Problem</a> | <span class="clickable" onclick="ShowInfo('termsText')" style="font-size:12px;">Terms</span> <span class="clickable" onclick="ShowInfo('privacyText')" style="font-size:12px;">Privacy</span>
</footer>
<!-- Markdown Parser -->
<script src="js/marked.js"></script>
<!-- JSON Search -->
<script src="js/defiant.js"></script>
<!-- Diacritics Removal for Exports -->
<script src="js/removeDiacritics.js"></script>
<!-- Helper Functions -->
<script src="js/helpers.js"></script>
<!-- Main Functions -->
<script src="js/dictionaryBuilder.js"></script>
<!-- UI Functions -->
<script src="js/ui.js"></script>
<?php if ($_GET['adminoverride'] != "noadsortracking") { include_once("php/google/analytics.php"); } ?>
<script>
var aboutText = termsText = privacyText = loginForm = forgotForm = "Loading...";
ready(function() {
Initialize();
});
</script>
</body>
</html>

View File

@ -1,687 +0,0 @@
/*
* Defiant.js v1.2.5
* Serch JSON structures plus smart templating with XSLT and XPath.
* http://defiantjs.com
*
* Copyright (c) 2013-2015, Hakan Bilgin <hbi@longscript.com>
* Licensed under the MIT License
*
* NOTE:
* Robbie Antenesse edited line 165's RegExp to search global and case-insensitive in case of multiple contains() groups.
*/
if (typeof module === "undefined") {
var module = { exports: undefined };
} else {
// Node env adaptation goes here...
}
module.exports = Defiant = (function(window, undefined) {
'use strict';
var Defiant = {
is_ie : /msie/i.test(navigator.userAgent),
is_safari : /safari/i.test(navigator.userAgent),
env : 'production',
xml_decl : '<?xml version="1.0" encoding="utf-8"?>',
namespace : 'xmlns:d="defiant-namespace"',
tabsize : 4,
render: function(template, data) {
var processor = new XSLTProcessor(),
span = document.createElement('span'),
opt = {match: '/'},
tmpltXpath,
scripts,
temp,
sorter;
// handle arguments
switch (typeof(template)) {
case 'object':
this.extend(opt, template);
if (!opt.data) opt.data = data;
break;
case 'string':
opt.template = template;
opt.data = data;
break;
default:
throw 'error';
}
opt.data = JSON.toXML(opt.data);
tmpltXpath = '//xsl:template[@name="'+ opt.template +'"]';
if (!this.xsl_template) this.gatherTemplates();
if (opt.sorter) {
sorter = this.node.selectSingleNode(this.xsl_template, tmpltXpath +'//xsl:for-each//xsl:sort');
if (sorter) {
if (opt.sorter.order) sorter.setAttribute('order', opt.sorter.order);
if (opt.sorter.select) sorter.setAttribute('select', opt.sorter.select);
sorter.setAttribute('data-type', opt.sorter.type || 'text');
}
}
temp = this.node.selectSingleNode(this.xsl_template, tmpltXpath);
temp.setAttribute('match', opt.match);
processor.importStylesheet(this.xsl_template);
span.appendChild(processor.transformToFragment(opt.data, document));
temp.removeAttribute('match');
if (this.is_safari) {
scripts = span.getElementsByTagName('script');
for (var i=0, il=scripts.length; i<il; i++) scripts[i].defer = true;
}
return span.innerHTML;
},
gatherTemplates: function() {
var scripts = document.getElementsByTagName('script'),
str = '',
i = 0,
il = scripts.length;
for (; i<il; i++) {
if (scripts[i].type === 'defiant/xsl-template') str += scripts[i].innerHTML;
}
this.xsl_template = this.xmlFromString('<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" '+ this.namespace +'>'+ str.replace(/defiant:(\w+)/g, '$1') +'</xsl:stylesheet>');
},
getSnapshot: function(data) {
return JSON.toXML(data, true);
},
xmlFromString: function(str) {
var parser,
doc;
str = str.replace(/>\s{1,}</g, '><');
if (str.trim().match(/<\?xml/) === null) {
str = this.xml_decl + str;
}
if (this.is_ie) {
doc = new ActiveXObject('Msxml2.DOMDocument');
doc.loadXML(str);
if (str.indexOf('xsl:stylesheet') === -1) {
doc.setProperty('SelectionLanguage', 'XPath');
}
} else {
parser = new DOMParser();
doc = parser.parseFromString(str, 'text/xml');
}
return doc;
},
extend: function(src, dest) {
for (var content in dest) {
if (!src[content] || typeof(dest[content]) !== 'object') {
src[content] = dest[content];
} else {
this.extend(src[content], dest[content]);
}
}
return src;
},
node: {}
};
return Defiant;
})(this);
if (typeof(XSLTProcessor) === 'undefined') {
// emulating XSLT Processor (enough to be used in defiant)
var XSLTProcessor = function() {};
XSLTProcessor.prototype = {
importStylesheet: function(xsldoc) {
this.xsldoc = xsldoc;
},
transformToFragment: function(data, doc) {
var str = data.transformNode(this.xsldoc),
span = document.createElement('span');
span.innerHTML = str;
return span;
}
};
}
// extending STRING
if (!String.prototype.fill) {
String.prototype.fill = function(i,c) {
var str = this;
c = c || ' ';
for (; str.length<i; str+=c){}
return str;
};
}
if (!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace(/^\s+|\s+$/gm, '');
};
}
if (!String.prototype.xTransform) {
String.prototype.xTransform = function () {
var str = this;
if (this.indexOf('translate(') === -1) {
str = this.replace(/contains\(([^,]+),([^\\)]+)\)/gi, function(c,h,n) {
var a = 'abcdefghijklmnopqrstuvwxyz',
q = n.trim().slice(-1);
return "contains(translate("+ h +", "+ q + a.toUpperCase() + q +", "+ q + a + q +"),"+ n.toLowerCase() +")";
});
}
return str.toString();
};
}
if (typeof(JSON) === 'undefined') {
window.JSON = {
parse: function (sJSON) { return eval("(" + sJSON + ")"); },
stringify: function (vContent) {
if (vContent instanceof Object) {
var sOutput = "";
if (vContent.constructor === Array) {
for (var nId = 0; nId < vContent.length; sOutput += this.stringify(vContent[nId]) + ",", nId++);
return "[" + sOutput.substr(0, sOutput.length - 1) + "]";
}
if (vContent.toString !== Object.prototype.toString) {
return "\"" + vContent.toString().replace(/"/g, "\\$&") + "\"";
}
for (var sProp in vContent) {
sOutput += "\"" + sProp.replace(/"/g, "\\$&") + "\":" + this.stringify(vContent[sProp]) + ",";
}
return "{" + sOutput.substr(0, sOutput.length - 1) + "}";
}
return typeof vContent === "string" ? "\"" + vContent.replace(/"/g, "\\$&") + "\"" : String(vContent);
}
};
}
/* jshint ignore:end */
if (!JSON.toXML) {
JSON.toXML = function(tree, snapshot) {
'use strict';
var interpreter = {
map : [],
rx_validate_name : /^(?!xml)[a-z_][\w\d.:]*$/i,
rx_node : /<(.+?)( .*?)>/,
rx_constructor : /<(.+?)( d:contr=".*?")>/,
rx_namespace : / xmlns\:d="defiant\-namespace"/,
rx_data : /(<.+?>)(.*?)(<\/d:data>)/i,
rx_function : /function (\w+)/i,
to_xml: function(tree) {
var str = this.hash_to_xml(null, tree);
return Defiant.xmlFromString(str);
},
hash_to_xml: function(name, tree, array_child) {
var is_array = tree.constructor === Array,
elem = [],
attr = [],
key,
val,
val_is_array,
type,
is_attr,
cname,
constr,
cnName,
i;
for (key in tree) {
val = tree[key];
if (val === null || val === undefined || val.toString() === 'NaN') val = null;
is_attr = key.slice(0,1) === '@';
cname = array_child ? name : key;
if (cname == +cname && tree.constructor !== Object) cname = 'd:item';
if (val === null) {
constr = null;
cnName = false;
} else {
constr = val.constructor;
cnName = constr.toString().match(this.rx_function)[1];
}
if (is_attr) {
attr.push( cname.slice(1) +'="'+ this.escape_xml(val) +'"' );
if (cnName !== 'String') attr.push( 'd:'+ cname.slice(1) +'="'+ cnName +'"' );
} else if (val === null) {
elem.push( this.scalar_to_xml( cname, val ) );
} else {
switch (constr) {
case Function:
// if constructor is function, then it's not a JSON structure
// throw ERROR ?
break;
case Object:
elem.push( this.hash_to_xml( cname, val ) );
break;
case Array:
if (key === cname) {
val_is_array = val.constructor === Array;
if (val_is_array) {
i = val.length;
while (i--) {
if (val[i].constructor === Array) val_is_array = true;
if (!val_is_array && val[i].constructor === Object) val_is_array = true;
}
}
elem.push( this.scalar_to_xml( cname, val, val_is_array ) );
break;
}
/* falls through */
case String:
if (typeof(val) === 'string') {
val = val.toString().replace(/\&/g, '&amp;')
.replace(/\r|\n/g, '&#13;');
}
if (cname === '#text') {
// prepare map
this.map.push(tree);
attr.push('d:mi="'+ this.map.length +'"');
attr.push('d:constr="'+ cnName +'"');
elem.push( this.escape_xml(val) );
break;
}
/* falls through */
case Number:
case Boolean:
if (cname === '#text' && cnName !== 'String') {
// prepare map
this.map.push(tree);
attr.push('d:mi="'+ this.map.length +'"');
attr.push('d:constr="'+ cnName +'"');
elem.push( this.escape_xml(val) );
break;
}
elem.push( this.scalar_to_xml( cname, val ) );
break;
}
}
}
if (!name) {
name = 'd:data';
attr.push(Defiant.namespace);
if (is_array) attr.push('d:constr="Array"');
}
if (name.match(this.rx_validate_name) === null) {
attr.push( 'd:name="'+ name +'"' );
name = 'd:name';
}
if (array_child) return elem.join('');
// prepare map
this.map.push(tree);
attr.push('d:mi="'+ this.map.length +'"');
return '<'+ name + (attr.length ? ' '+ attr.join(' ') : '') + (elem.length ? '>'+ elem.join('') +'</'+ name +'>' : '/>' );
},
scalar_to_xml: function(name, val, override) {
var attr = '',
text,
constr,
cnName;
// check whether the nodename is valid
if (name.match(this.rx_validate_name) === null) {
attr += ' d:name="'+ name +'"';
name = 'd:name';
override = false;
}
if (val === null || val.toString() === 'NaN') val = null;
if (val === null) return '<'+ name +' d:constr="null"/>';
if (val.length === 1 && val[0].constructor === Object) {
text = this.hash_to_xml(false, val[0]);
var a1 = text.match(this.rx_node),
a2 = text.match(this.rx_constructor);
a1 = (a1 !== null)? a1[2]
.replace(this.rx_namespace, '')
.replace(/>/, '')
.replace(/"\/$/, '"') : '';
a2 = (a2 !== null)? a2[2] : '';
text = text.match(this.rx_data);
text = (text !== null)? text[2] : '';
return '<'+ name + a1 +' '+ a2 +' d:type="ArrayItem">'+ text +'</'+ name +'>';
} else if (val.length === 0 && val.constructor === Array) {
return '<'+ name +' d:constr="Array"/>';
}
// else
if (override) {
return this.hash_to_xml( name, val, true );
}
constr = val.constructor;
cnName = constr.toString().match(this.rx_function)[1];
text = (constr === Array) ? this.hash_to_xml( 'd:item', val, true )
: this.escape_xml(val);
attr += ' d:constr="'+ cnName +'"';
// prepare map
this.map.push(val);
attr += ' d:mi="'+ this.map.length +'"';
return (name === '#text') ? this.escape_xml(val) : '<'+ name + attr +'>'+ text +'</'+ name +'>';
},
escape_xml: function(text) {
return String(text) .replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/&nbsp;/g, '&#160;');
}
},
doc = interpreter.to_xml.call(interpreter, tree);
// snapshot distinctly improves performance
if (snapshot) {
return {
doc: doc,
src: tree,
map: interpreter.map
};
}
this.search.map = interpreter.map;
return doc;
};
}
if (!JSON.search) {
JSON.search = function(tree, xpath, single) {
'use strict';
var isSnapshot = tree.doc && tree.doc.nodeType,
doc = isSnapshot ? tree.doc : JSON.toXML(tree),
map = isSnapshot ? tree.map : this.search.map,
src = isSnapshot ? tree.src : tree,
xres = Defiant.node[ single ? 'selectSingleNode' : 'selectNodes' ](doc, xpath.xTransform()),
ret = [],
mapIndex,
i;
if (single) xres = [xres];
i = xres.length;
while (i--) {
switch(xres[i].nodeType) {
case 2:
case 3:
ret.unshift( xres[i].nodeValue );
break;
default:
mapIndex = +xres[i].getAttribute('d:mi');
if (map[mapIndex-1]) ret.unshift( map[mapIndex-1] );
}
}
// if environment = development, add search tracing
if (Defiant.env === 'development') {
this.trace = JSON.mtrace(src, ret, xres);
}
return ret;
};
}
if (!JSON.mtrace) {
JSON.mtrace = function(root, hits, xres) {
'use strict';
var win = window,
stringify = JSON.stringify,
sroot = stringify( root, null, '\t' ).replace(/\t/g, ''),
trace = [],
i = 0,
il = xres.length,
od = il ? xres[i].ownerDocument.documentElement : false,
map = this.search.map,
hstr,
cConstr,
fIndex = 0,
mIndex,
lStart,
lEnd;
for (; i<il; i++) {
switch (xres[i].nodeType) {
case 2:
cConstr = xres[i].ownerElement ? xres[i].ownerElement.getAttribute('d:'+ xres[i].nodeName) : 'String';
hstr = '"@'+ xres[i].nodeName +'": '+ win[ cConstr ]( hits[i] );
mIndex = sroot.indexOf(hstr);
lEnd = 0;
break;
case 3:
cConstr = xres[i].parentNode.getAttribute('d:constr');
hstr = win[ cConstr ]( hits[i] );
hstr = '"'+ xres[i].parentNode.nodeName +'": '+ (hstr === 'Number' ? hstr : '"'+ hstr +'"');
mIndex = sroot.indexOf(hstr);
lEnd = 0;
break;
default:
if (xres[i] === od) continue;
if (xres[i].getAttribute('d:constr') === 'String') {
cConstr = xres[i].getAttribute('d:constr');
hstr = win[ cConstr ]( hits[i] );
hstr = '"'+ xres[i].nodeName +'": '+ (hstr === 'Number' ? hstr : '"'+ hstr +'"');
mIndex = sroot.indexOf(hstr, fIndex);
lEnd = 0;
fIndex = mIndex + 1;
} else {
hstr = stringify( hits[i], null, '\t' ).replace(/\t/g, '');
mIndex = sroot.indexOf(hstr);
lEnd = hstr.match(/\n/g).length;
}
}
lStart = sroot.substring(0,mIndex).match(/\n/g).length+1;
trace.push([lStart, lEnd]);
}
return trace;
};
}
Defiant.node.selectNodes = function(XNode, XPath) {
if (XNode.evaluate) {
var ns = XNode.createNSResolver(XNode.documentElement),
qI = XNode.evaluate(XPath, XNode, ns, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null),
res = [],
i = 0,
il = qI.snapshotLength;
for (; i<il; i++) {
res.push( qI.snapshotItem(i) );
}
return res;
} else {
return XNode.selectNodes(XPath);
}
};
Defiant.node.selectSingleNode = function(XNode, XPath) {
if (XNode.evaluate) {
var xI = this.selectNodes(XNode, XPath);
return (xI.length > 0)? xI[0] : null;
} else {
return XNode.selectSingleNode(XPath);
}
};
Defiant.node.prettyPrint = function(node) {
var root = Defiant,
tabs = root.tabsize,
decl = root.xml_decl.toLowerCase(),
ser,
xstr;
if (root.is_ie) {
xstr = node.xml;
} else {
ser = new XMLSerializer();
xstr = ser.serializeToString(node);
}
if (root.env !== 'development') {
// if environment is not development, remove defiant related info
xstr = xstr.replace(/ \w+\:d=".*?"| d\:\w+=".*?"/g, '');
}
var str = xstr.trim().replace(/(>)\s*(<)(\/*)/g, '$1\n$2$3'),
lines = str.split('\n'),
indent = -1,
i = 0,
il = lines.length,
start,
end;
for (; i<il; i++) {
if (i === 0 && lines[i].toLowerCase() === decl) continue;
start = lines[i].match(/<[A-Za-z_\:]+.*?>/g) !== null;
//start = lines[i].match(/<[^\/]+>/g) !== null;
end = lines[i].match(/<\/[\w\:]+>/g) !== null;
if (lines[i].match(/<.*?\/>/g) !== null) start = end = true;
if (start) indent++;
lines[i] = String().fill(indent, '\t') + lines[i];
if (start && end) indent--;
if (!start && end) indent--;
}
return lines.join('\n').replace(/\t/g, String().fill(tabs, ' '));
};
Defiant.node.toJSON = function(xnode, stringify) {
'use strict';
var interpret = function(leaf) {
var obj = {},
win = window,
attr,
type,
item,
cname,
cConstr,
cval,
text,
i, il, a;
switch (leaf.nodeType) {
case 1:
cConstr = leaf.getAttribute('d:constr');
if (cConstr === 'Array') obj = [];
else if (cConstr === 'String' && leaf.textContent === '') obj = '';
attr = leaf.attributes;
i = 0;
il = attr.length;
for (; i<il; i++) {
a = attr.item(i);
if (a.nodeName.match(/\:d|d\:/g) !== null) continue;
cConstr = leaf.getAttribute('d:'+ a.nodeName);
if (cConstr && cConstr !== 'undefined') {
if (a.nodeValue === 'null') cval = null;
else cval = win[ cConstr ]( (a.nodeValue === 'false') ? '' : a.nodeValue );
} else {
cval = a.nodeValue;
}
obj['@'+ a.nodeName] = cval;
}
break;
case 3:
type = leaf.parentNode.getAttribute('d:type');
cval = (type) ? win[ type ]( leaf.nodeValue === 'false' ? '' : leaf.nodeValue ) : leaf.nodeValue;
obj = cval;
break;
}
if (leaf.hasChildNodes()) {
i = 0;
il = leaf.childNodes.length;
for(; i<il; i++) {
item = leaf.childNodes.item(i);
cname = item.nodeName;
attr = leaf.attributes;
if (cname === 'd:name') {
cname = item.getAttribute('d:name');
}
if (cname === '#text') {
cConstr = leaf.getAttribute('d:constr');
if (cConstr === 'undefined') cConstr = undefined;
text = item.textContent || item.text;
cval = cConstr === 'Boolean' && text === 'false' ? '' : text;
if (!cConstr && !attr.length) obj = cval;
else if (cConstr && il === 1) {
obj = win[cConstr](cval);
} else if (!leaf.hasChildNodes()) {
obj[cname] = (cConstr)? win[cConstr](cval) : cval;
} else {
if (attr.length < 3) obj = (cConstr)? win[cConstr](cval) : cval;
else obj[cname] = (cConstr)? win[cConstr](cval) : cval;
}
} else {
if (obj[cname]) {
if (obj[cname].push) obj[cname].push( interpret(item) );
else obj[cname] = [obj[cname], interpret(item)];
continue;
}
cConstr = item.getAttribute('d:constr');
switch (cConstr) {
case 'null':
if (obj.push) obj.push(null);
else obj[cname] = null;
break;
case 'Array':
//console.log( Defiant.node.prettyPrint(item) );
if (item.parentNode.firstChild === item && cConstr === 'Array' && cname !== 'd:item') {
if (cname === 'd:item' || cConstr === 'Array') {
cval = interpret(item);
obj[cname] = cval.length ? [cval] : cval;
} else {
obj[cname] = interpret(item);
}
}
else if (obj.push) obj.push( interpret(item) );
else obj[cname] = interpret(item);
break;
case 'String':
case 'Number':
case 'Boolean':
text = item.textContent || item.text;
cval = cConstr === 'Boolean' && text === 'false' ? '' : text;
if (obj.push) obj.push( win[cConstr](cval) );
else obj[cname] = interpret(item);
break;
default:
if (obj.push) obj.push( interpret( item ) );
else obj[cname] = interpret( item );
}
}
}
}
if (leaf.nodeType === 1 && leaf.getAttribute('d:type') === 'ArrayItem') {
obj = [obj];
}
return obj;
},
node = (xnode.nodeType === 9) ? xnode.documentElement : xnode,
ret = interpret(node),
rn = ret[node.nodeName];
// exclude root, if "this" is root node
if (node === node.ownerDocument.documentElement && rn && rn.constructor === Array) {
ret = rn;
}
if (stringify && stringify.toString() === 'true') stringify = '\t';
return stringify ? JSON.stringify(ret, null, stringify) : ret;
};
// check if jQuery is present
if (typeof(jQuery) !== 'undefined') {
(function ( $ ) {
'use strict';
$.fn.defiant = function(template, xpath) {
this.html( Defiant.render(template, xpath) );
return this;
};
}(jQuery));
}

View File

@ -1,669 +0,0 @@
/* global markdown */
/* global Defiant */
var publicName = "Someone";
var currentDictionary = {
name: "New",
description: "A new dictionary.",
createdBy: publicName,
words: [],
nextWordId: 1,
settings: {
allowDuplicates: false,
caseSensitive: false,
partsOfSpeech: "Noun,Adjective,Verb,Adverb,Preposition,Pronoun,Conjunction",
sortByEquivalent: false,
isComplete: false,
isPublic: false
},
externalID: 0
}
var defaultDictionaryJSON = JSON.stringify(currentDictionary); //Saves a stringifyed default dictionary.
var previousDictionary = {};
var savedScroll = {
x: 0,
y: 0
}
function AddWord() {
var word = htmlEntities(document.getElementById("word").value).trim();
var pronunciation = htmlEntities(document.getElementById("pronunciation").value).trim();
var partOfSpeech = htmlEntities(document.getElementById("partOfSpeech").value).trim();
var simpleDefinition = htmlEntities(document.getElementById("simpleDefinition").value).trim();
var longDefinition = htmlEntities(document.getElementById("longDefinition").value);
var editIndex = htmlEntities(document.getElementById("editIndex").value);
var errorMessageArea = document.getElementById("errorMessage");
var errorMessage = "";
var updateConflictArea = document.getElementById("updateConflict");
if (word != "" && (simpleDefinition != "" || longDefinition != "")) {
var wordIndex = (!currentDictionary.settings.allowDuplicates) ? WordIndex(word) : -1;
if (editIndex != "") {
if (WordAtIndexWasChanged(editIndex, word, pronunciation, partOfSpeech, simpleDefinition, longDefinition)) {
document.getElementById("editWordButtonArea").style.display = "none";
DisableForm();
updateConflictArea.style.display = "block";
updateConflictArea.innerHTML = "<span id='updateConflictMessage'>Do you really want to change the word \"" + currentDictionary.words[parseInt(editIndex)].name + "\" to what you have set above?</span>";
updateConflictArea.innerHTML += '<button type="button" id="updateConfirmButton" \
onclick="UpdateWord(' + editIndex + ', \'' + htmlEntities(word) + '\', \'' + htmlEntities(pronunciation) + '\', \'' + htmlEntities(partOfSpeech) + '\', \'' + htmlEntities(simpleDefinition) + '\', \'' + htmlEntities(longDefinition) + '\'); \
return false;">Yes, Update it</button>';
updateConflictArea.innerHTML += '<button type="button" id="updateCancelButton" onclick="CloseUpdateConflictArea(\'editWordButtonArea\'); return false;">No, Leave it</button>';
} else {
errorMessage = "No change has been made to \"" + word + "\"";
if (currentDictionary.words[parseInt(editIndex)].name != word) {
errorMessage += ". (Your dictionary is currently set to ignore case.)"
}
}
} else if (wordIndex >= 0) {
if (WordAtIndexWasChanged(wordIndex, word, pronunciation, partOfSpeech, simpleDefinition, longDefinition)) {
document.getElementById("newWordButtonArea").style.display = "none";
DisableForm();
updateConflictArea.style.display = "block";
var updateConflictText = "<span id='updateConflictMessage'>\"" + word + "\" is already in the dictionary";
if (currentDictionary.words[wordIndex].name != word) {
updateConflictText += " as \"" + currentDictionary.words[wordIndex].name + "\", and your dictionary is set to ignore case.";
} else {
updateConflictText += "."
}
updateConflictText += "<br>Do you want to update it to what you have set above?</span>";
updateConflictText += '<button type="button" id="updateConfirmButton" \
onclick="UpdateWord(' + wordIndex + ', \'' + htmlEntities(word) + '\', \'' + htmlEntities(pronunciation) + '\', \'' + htmlEntities(partOfSpeech) + '\', \'' + htmlEntities(simpleDefinition) + '\', \'' + htmlEntities(longDefinition) + '\'); \
return false;">Yes, Update it</button>';
updateConflictText += ' <button type="button" id="updateCancelButton" onclick="CloseUpdateConflictArea(\'newWordButtonArea\'); return false;">No, Leave it</button>';
updateConflictArea.innerHTML = updateConflictText;
} else {
errorMessage = "\"" + word + "\" is already in the dictionary exactly as it is written above";
if (currentDictionary.words[wordIndex].name != word) {
errorMessage += ". (Your dictionary is currently set to ignore case.)"
}
}
} else {
currentDictionary.words.push({name: word, pronunciation: pronunciation, partOfSpeech: partOfSpeech, simpleDefinition: simpleDefinition, longDefinition: longDefinition, wordId: currentDictionary.nextWordId++});
FocusAfterAddingNewWord();
NewWordNotification(word);
SaveAndUpdateDictionary(false);
}
errorMessageArea.innerHTML = "";
} else {
if (word == "") {
errorMessage += "Word cannot be blank";
if (simpleDefinition == "" && longDefinition == "") {
errorMessage += " and you need at least one definition.";
} else {
errorMessage += ".";
}
} else if (simpleDefinition == "" && longDefinition == "") {
errorMessage += "You need at least one definition."
}
}
errorMessageArea.innerHTML = errorMessage;
}
function EditWord(index) {
SaveScroll();
if (wordFormIsLocked()) {
window.scroll(0, 0);
}
ClearForm();
document.getElementById("editIndex").value = index.toString();
document.getElementById("word").value = htmlEntitiesParse(currentDictionary.words[index].name);
document.getElementById("pronunciation").value = htmlEntitiesParse(currentDictionary.words[index].pronunciation);
document.getElementById("partOfSpeech").value = htmlEntitiesParse(currentDictionary.words[index].partOfSpeech);
document.getElementById("simpleDefinition").value = htmlEntitiesParse(currentDictionary.words[index].simpleDefinition);
document.getElementById("longDefinition").value = htmlEntitiesParse(currentDictionary.words[index].longDefinition);
document.getElementById("newWordButtonArea").style.display = "none";
document.getElementById("editWordButtonArea").style.display = "block";
}
function UpdateWord(wordIndex, word, pronunciation, partOfSpeech, simpleDefinition, longDefinition) {
currentDictionary.words[wordIndex].name = word;
currentDictionary.words[wordIndex].pronunciation = pronunciation;
currentDictionary.words[wordIndex].partOfSpeech = partOfSpeech;
currentDictionary.words[wordIndex].simpleDefinition = simpleDefinition;
currentDictionary.words[wordIndex].longDefinition = longDefinition;
SaveAndUpdateDictionary();
window.scroll(savedScroll.x, savedScroll.y);
if (!wordFormIsLocked()) {
FocusAfterAddingNewWord();
}
}
function DeleteWord(index) {
if (document.getElementById("editIndex").value != "")
ClearForm();
currentDictionary.words.splice(index, 1);
SaveAndUpdateDictionary(true);
}
function ShowDictionary() {
var filters = GetSelectedFilters();
var searchResults = [];
var search = htmlEntitiesParseForSearchEntry(document.getElementById("searchBox").value);
var searchByWord = document.getElementById("searchOptionWord").checked;
var searchBySimple = document.getElementById("searchOptionSimple").checked;
var searchByLong = document.getElementById("searchOptionLong").checked;
var searchIgnoreCase = !document.getElementById("searchCaseSensitive").checked; //It's easier to negate case here instead of negating it every use since ignore case is default.
var searchIgnoreDiacritics = document.getElementById("searchIgnoreDiacritics").checked;
if (search != "" && (searchByWord || searchBySimple || searchByLong)) {
var xpath = [];
var searchDictionaryJSON = htmlEntitiesParseForSearch(JSON.stringify(currentDictionary));
if (searchIgnoreCase) {
search = search.toLowerCase();
}
if (searchIgnoreDiacritics) {
search = removeDiacritics(search);
searchDictionaryJSON = removeDiacritics(searchDictionaryJSON);
}
if (searchByWord) {
xpath.push('contains('+ ((searchIgnoreCase) ? 'name' : 'translate(name, "", "")') +', "'+ search +'")');
}
if (searchBySimple) {
xpath.push('contains('+ ((searchIgnoreCase) ? 'simpleDefinition' : 'translate(simpleDefinition, "", "")') +', "'+ search +'")');
}
if (searchByLong) {
xpath.push('contains('+ ((searchIgnoreCase) ? 'longDefinition' : 'translate(longDefinition, "", "")') +', "'+ search +'")');
}
var searchDictionary = JSON.parse(searchDictionaryJSON);
searchResults = JSON.search(searchDictionary, '//words['+ xpath.join(' or ') +']/wordId');
}
var dictionaryNameArea = document.getElementById("dictionaryName");
dictionaryNameArea.innerHTML = htmlEntitiesParse(currentDictionary.name) + " Dictionary";
var dictionaryDescriptionArea = document.getElementById("dictionaryDescription");
dictionaryDescriptionArea.innerHTML = marked(htmlEntitiesParse(currentDictionary.description));
var dictionaryArea = document.getElementById("theDictionary");
var dictionaryText = "";
var numberOfWordsDisplayed = 0;
if (currentDictionary.words.length > 0) {
for (var i = 0; i < currentDictionary.words.length; i++) {
if (filters.length == 0 || (filters.length > 0 && filters.indexOf(currentDictionary.words[i].partOfSpeech) > -1)) {
if (search == "" || (search != "" && (searchByWord || searchBySimple || searchByLong) && searchResults.indexOf(currentDictionary.words[i].wordId) >= 0)) {
if (!currentDictionary.words[i].hasOwnProperty("pronunciation")) {
currentDictionary.words[i].pronunciation = ""; //Account for new property
}
if (!currentDictionary.words[i].hasOwnProperty("wordId")) {
currentDictionary.words[i].wordId = i + 1; //Account for new property
}
dictionaryText += DictionaryEntry(i);
numberOfWordsDisplayed++;
}
}
}
} else {
dictionaryText = "There are no entries in the dictionary."
}
dictionaryArea.innerHTML = dictionaryText;
ShowFilterWordCount(numberOfWordsDisplayed);
}
function DictionaryEntry(itemIndex) {
displayPublic = (typeof displayPublic !== 'undefined' && displayPublic != null) ? displayPublic : false;
var entryText = "<entry><a name='" + currentDictionary.words[itemIndex].wordId + "'></a><a href='#" + currentDictionary.words[itemIndex].wordId + "' class='wordLink clickable'>&#x1f517;</a>";
var searchTerm = regexParseForSearch(document.getElementById("searchBox").value);
var searchByWord = document.getElementById("searchOptionWord").checked;
var searchBySimple = document.getElementById("searchOptionSimple").checked;
var searchByLong = document.getElementById("searchOptionLong").checked;
var searchIgnoreCase = !document.getElementById("searchCaseSensitive").checked; //It's easier to negate case here instead of negating it every use since ignore case is default.
var searchIgnoreDiacritics = document.getElementById("searchIgnoreDiacritics").checked;
var searchRegEx = new RegExp("(" + ((searchIgnoreDiacritics) ? removeDiacritics(searchTerm) + "|" + searchTerm : searchTerm) + ")", "g" + ((searchIgnoreCase) ? "i" : ""));
entryText += "<word>";
if (searchTerm != "" && searchByWord) {
entryText += htmlEntitiesParse(currentDictionary.words[itemIndex].name).replace(searchRegEx, "<searchTerm>$1</searchterm>");
} else {
entryText += currentDictionary.words[itemIndex].name;
}
entryText += "</word>";
if (currentDictionary.words[itemIndex].pronunciation != "") {
entryText += "<pronunciation>";
entryText += marked(htmlEntitiesParse(currentDictionary.words[itemIndex].pronunciation)).replace("<p>","").replace("</p>","");
entryText += "</pronunciation>";
}
if (currentDictionary.words[itemIndex].partOfSpeech != "") {
entryText += "<partofspeech>";
entryText += currentDictionary.words[itemIndex].partOfSpeech;
entryText += "</partofspeech>";
}
entryText += "<br>";
if (currentDictionary.words[itemIndex].simpleDefinition != "") {
entryText += "<simpledefinition>";
if (searchTerm != "" && searchBySimple) {
entryText += htmlEntitiesParse(currentDictionary.words[itemIndex].simpleDefinition).replace(searchRegEx, "<searchTerm>$1</searchterm>");
} else {
entryText += currentDictionary.words[itemIndex].simpleDefinition;
}
entryText += "</simpledefinition>";
}
if (currentDictionary.words[itemIndex].longDefinition != "") {
entryText += "<longdefinition>";
if (searchTerm != "" && searchByLong) {
entryText += marked(htmlEntitiesParse(currentDictionary.words[itemIndex].longDefinition).replace(searchRegEx, "<searchTerm>$1</searchterm>"));
} else {
entryText += marked(htmlEntitiesParse(currentDictionary.words[itemIndex].longDefinition));
}
entryText += "</longdefinition>";
}
if (!currentDictionary.settings.isComplete) {
entryText += ManagementArea(itemIndex);
}
entryText += "</entry>";
return entryText;
}
function ManagementArea(itemIndex) {
var managementHTML = "<div class='management'>";
managementHTML += "<span class='clickable editButton' onclick='EditWord(" + itemIndex + ")'>Edit</span>";
managementHTML += "<span class='clickable deleteButton' onclick='document.getElementById(\"delete" + itemIndex + "Confirm\").style.display = \"block\";'>Delete</span>";
managementHTML += "<div class='deleteConfirm' id='delete" + itemIndex + "Confirm' style='display:none;'>Are you sure you want to delete this entry?<br><br>";
managementHTML += "<span class='clickable deleteCancelButton' onclick='document.getElementById(\"delete" + itemIndex + "Confirm\").style.display = \"none\";'>No</span>";
managementHTML += "<span class='clickable deleteConfirmButton' onclick='DeleteWord(" + itemIndex + ")'>Yes</span>";
managementHTML += "</div>";
managementHTML += "</div>";
return managementHTML;
}
function SaveSettings() {
if (htmlEntities(document.getElementById("dictionaryNameEdit").value) != "") {
currentDictionary.name = htmlEntities(document.getElementById("dictionaryNameEdit").value);
}
currentDictionary.description = htmlEntities(document.getElementById("dictionaryDescriptionEdit").value);
CheckForPartsOfSpeechChange();
currentDictionary.settings.allowDuplicates = document.getElementById("dictionaryAllowDuplicates").checked;
currentDictionary.settings.caseSensitive = document.getElementById("dictionaryCaseSensitive").checked;
currentDictionary.settings.sortByEquivalent = document.getElementById("dictionarySortByEquivalent").checked;
currentDictionary.settings.isComplete = document.getElementById("dictionaryIsComplete").checked;
if (document.getElementById("dictionaryIsPublic")) {
currentDictionary.settings.isPublic = document.getElementById("dictionaryIsPublic").checked;
}
HideSettingsWhenComplete();
SaveAndUpdateDictionary(true);
LoadUserDictionaries();
}
function EmptyWholeDictionary() {
if (confirm("This will delete the entire current dictionary. If you do not have a backed up export, you will lose it forever!\n\nDo you still want to delete?")) {
CreateNewDictionary();
}
}
function CreateNewDictionary() {
ResetDictionaryToDefault();
SaveAndUpdateDictionary(false);
SetPartsOfSpeech();
HideSettings();
}
function DeleteCurrentDictionary() {
if (confirm("This will delete the current dictionary from the database. If you do not have a backed up export, you will lose it forever!\n\nDo you still want to delete?")) {
ResetDictionaryToDefault();
var deleteDictionary = new XMLHttpRequest();
deleteDictionary.open('POST', "php/ajax_dictionarymanagement.php?action=delete");
deleteDictionary.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
deleteDictionary.onreadystatechange = function() {
if (deleteDictionary.readyState == 4 && deleteDictionary.status == 200) {
if (deleteDictionary.responseText.length < 31) {
console.log(deleteDictionary.responseText);
CreateNewDictionary();
} else {
HideSettings();
ShowDictionaryDeleteMenu(deleteDictionary.responseText);
if (document.getElementById("loadAfterDelete").options.length == 0) {
document.getElementById('loadAfterDeleteScreen').style.display = 'none';
CreateNewDictionary();
}
}
return true;
} else {
return false;
}
}
deleteDictionary.send();
}
}
function ResetDictionaryToDefault() {
currentDictionary = JSON.parse(defaultDictionaryJSON);
}
function SaveAndUpdateDictionary(keepFormContents) {
if (!currentDictionary.settings.sortByEquivalent) {
currentDictionary.words.sort(dynamicSort(['name', 'partOfSpeech']));
} else {
currentDictionary.words.sort(dynamicSort(['simpleDefinition', 'partOfSpeech']));
}
SaveDictionary(true, true);
ShowDictionary();
if (!keepFormContents) {
ClearForm();
}
CloseUpdateConflictArea('newWordButtonArea');
}
function SaveDictionary(sendToDatabase, sendWords) {
localStorage.setItem('dictionary', JSON.stringify(currentDictionary));
//Always save local copy of current dictionary, but if logged in also send to database.
if (sendToDatabase) {
sendWords = (typeof sendWords !== 'undefined') ? sendWords : false;
SendDictionary(sendWords);
}
SavePreviousDictionary();
}
function SendDictionary(sendWords) {
sendWords = (typeof sendWords !== 'undefined') ? sendWords : false;
var action = "";
var postString = "";
if (currentDictionary.externalID > 0) {
action = "update";
postString = DataToSend(sendWords);
} else {
action = "new";
postString = DataToSend(true, true);
}
var sendDictionary = new XMLHttpRequest();
sendDictionary.open('POST', "php/ajax_dictionarymanagement.php?action=" + action);
sendDictionary.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
sendDictionary.onreadystatechange = function() {
if (sendDictionary.readyState == 4 && sendDictionary.status == 200) {
if (sendDictionary.responseText == "updated successfully") {
console.log(sendDictionary.responseText);
LoadUserDictionaries();
ProcessLoad();
} else if (isNaN(parseInt(sendDictionary.responseText))) {
console.log(sendDictionary.responseText);
} else { // It will only be a number if it is a new dictionary.
currentDictionary.externalID = parseInt(sendDictionary.responseText);
LoadUserDictionaries();
ProcessLoad();
console.log("saved successfully");
}
return true;
} else {
return false;
}
}
sendDictionary.send(postString);
}
function DataToSend(doSendWords, sendAll) {
sendAll = (typeof sendAll !== 'undefined' && sendAll != null) ? sendAll : false;
var data = "";
if (currentDictionary.externalID == 0) {
data = "name=" + encodeURIComponent(currentDictionary.name) + "&description=" + encodeURIComponent(currentDictionary.description) + "&words=" + encodeURIComponent(JSON.stringify(currentDictionary.words));
data += "&nextwordid=" + currentDictionary.nextWordId + "&allowduplicates=" + ((currentDictionary.settings.allowDuplicates) ? "1" : "0") + "&casesensitive=" + ((currentDictionary.settings.caseSensitive) ? "1" : "0");
data += "&partsofspeech=" + encodeURIComponent(currentDictionary.settings.partsOfSpeech) + "&sortbyequivalent=" + ((currentDictionary.settings.sortByEquivalent) ? "1" : "0") + "&iscomplete=" + ((currentDictionary.settings.isComplete) ? "1" : "0") + "&ispublic=" + ((currentDictionary.settings.isPublic) ? "1" : "0") + "";
} else {
if (sendAll || currentDictionary.name != previousDictionary.name) {
data += "name=" + encodeURIComponent(currentDictionary.name);
}
if (sendAll || currentDictionary.description != previousDictionary.description) {
data += ((data=="") ? "" : "&") + "description=" + encodeURIComponent(currentDictionary.description);
}
if (sendAll || doSendWords) {
data += ((data=="") ? "" : "&") + "words=" + encodeURIComponent(JSON.stringify(currentDictionary.words));
}
if (sendAll || currentDictionary.nextWordId != previousDictionary.nextWordId) {
data += ((data=="") ? "" : "&") + "nextwordid=" + currentDictionary.nextWordId;
}
if (sendAll || currentDictionary.settings.allowDuplicates != previousDictionary.allowDuplicates) {
data += ((data=="") ? "" : "&") + "allowduplicates=" + ((currentDictionary.settings.allowDuplicates) ? "1" : "0");
}
if (sendAll || currentDictionary.settings.caseSensitive != previousDictionary.caseSensitive) {
data += ((data=="") ? "" : "&") + "casesensitive=" + ((currentDictionary.settings.caseSensitive) ? "1" : "0");
}
if (sendAll || currentDictionary.settings.partsOfSpeech != previousDictionary.partsOfSpeech) {
data += ((data=="") ? "" : "&") + "partsofspeech=" + encodeURIComponent(currentDictionary.settings.partsOfSpeech);
}
if (sendAll || currentDictionary.settings.sortByEquivalent != previousDictionary.sortByEquivalent) {
data += ((data=="") ? "" : "&") + "sortbyequivalent=" + ((currentDictionary.settings.sortByEquivalent) ? "1" : "0");
}
if (sendAll || currentDictionary.settings.isComplete != previousDictionary.isComplete) {
data += ((data=="") ? "" : "&") + "iscomplete=" + ((currentDictionary.settings.isComplete) ? "1" : "0");
}
if (sendAll || currentDictionary.settings.isPublic != previousDictionary.isPublic) {
data += ((data=="") ? "" : "&") + "ispublic=" + ((currentDictionary.settings.isPublic) ? "1" : "0");
}
}
return data;
}
function LoadDictionary() {
LoadLocalDictionary();
var loadDictionary = new XMLHttpRequest();
loadDictionary.open('GET', "php/ajax_dictionarymanagement.php?action=load");
loadDictionary.onreadystatechange = function() {
if (loadDictionary.readyState == 4 && loadDictionary.status == 200) {
if (loadDictionary.responseText == "no dictionaries") {
// If there are no dictionaries in the database and there's one in memory, remove the id & public setting and send it as a new one.
currentDictionary.externalID = 0;
currentDictionary.settings.isPublic = false;
SendDictionary(true);
} else if (loadDictionary.responseText.length < 60) {
console.log(loadDictionary.responseText);
} else {
currentDictionary = JSON.parse(loadDictionary.responseText);
SaveDictionary(false, false);
}
}
ProcessLoad();
}
loadDictionary.send();
}
function ChangeDictionary(userDictionariesSelect) {
userDictionariesSelect = (typeof userDictionariesSelect !== 'undefined' && userDictionariesSelect != null) ? userDictionariesSelect : document.getElementById("userDictionaries");
if (currentDictionary.externalID != userDictionariesSelect.value && userDictionariesSelect.options.length > 0) {
var changeDictionaryRequest = new XMLHttpRequest();
changeDictionaryRequest.open('POST', "php/ajax_dictionarymanagement.php?action=switch");
changeDictionaryRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
var postString = "newdictionaryid=" + userDictionariesSelect.value.toString();
changeDictionaryRequest.onreadystatechange = function() {
if (changeDictionaryRequest.readyState == 4 && changeDictionaryRequest.status == 200) {
if (changeDictionaryRequest.responseText == "no dictionaries") {
console.log(changeDictionaryRequest.responseText);
SendDictionary(false);
} else if (changeDictionaryRequest.responseText.length < 60) {
console.log(changeDictionaryRequest.responseText);
} else {
currentDictionary = JSON.parse(changeDictionaryRequest.responseText);
SaveDictionary(false, false);
ProcessLoad();
LoadUserDictionaries();
HideSettings();
}
}
}
changeDictionaryRequest.send(postString);
}
}
function LoadLocalDictionary() {
if (localStorage.getItem('dictionary')) {
var tmpDictionary = JSON.parse(localStorage.getItem('dictionary'));
if (tmpDictionary.words.length > 0 || tmpDictionary.description != "A new dictionary." || tmpDictionary.name != "New") {
currentDictionary = JSON.parse(localStorage.getItem('dictionary'));
}
tmpDictionary = null;
}
}
function ProcessLoad() {
if (!currentDictionary.hasOwnProperty("nextWordId")) {
currentDictionary.nextWordId = currentDictionary.words.length + 1;
}
HideSettingsWhenComplete();
ShowDictionary();
SetPartsOfSpeech();
if (currentDictionary.settings.isComplete) {
document.getElementById("wordEntryForm").style.display = "none";
}
SavePreviousDictionary();
}
function SavePreviousDictionary () {
// Save non-word data to check if anything has changed (words can identify themselves if changed).
// Used to minimize data pushed to database.
previousDictionary = {
name: currentDictionary.name,
description: currentDictionary.description,
nextWordId: currentDictionary.nextWordId,
allowDuplicates: currentDictionary.settings.allowDuplicates,
caseSensitive: currentDictionary.settings.caseSensitive,
partsOfSpeech: currentDictionary.settings.partsOfSpeech,
sortByEquivalent: currentDictionary.settings.sortByEquivalent,
isComplete: currentDictionary.settings.isComplete,
isPublic: currentDictionary.settings.isPublic
};
}
function ExportDictionary() {
if (currentDictionary.words.length > 0) {
var downloadName = removeDiacritics(stripHtmlEntities(currentDictionary.name)).replace(/\W/g, '');
if (downloadName == "") {
downloadName = "export";
}
download(downloadName + ".dict", localStorage.getItem('dictionary'));
} else {
alert("Dictionary must have at least 1 word to export.")
}
}
function ImportDictionary() {
if (currentDictionary.externalID > 0 || confirm("Importing this dictionary will overwrite your current one, making it impossible to retrieve if you have not already exported it! Do you still want to import?")) {
if (!window.FileReader) {
alert('Your browser is not supported');
return false;
}
var reader = new FileReader();
if (document.getElementById("importFile").files.length > 0) {
var file = document.getElementById("importFile").files[0];
// Read the file
reader.readAsText(file);
// When it's loaded, process it
reader.onloadend = function () {
if (reader.result && reader.result.length) {
var tmpDicitonary = JSON.parse(reader.result);
if (tmpDicitonary.hasOwnProperty("name") && tmpDicitonary.hasOwnProperty("description") &&
tmpDicitonary.hasOwnProperty("words") && tmpDicitonary.hasOwnProperty("settings"))
{
currentDictionary = JSON.parse(reader.result);
currentDictionary.externalID = 0; // Reset external id for imported dictionary.
currentDictionary.settings.isPublic = false; // Reset public setting for imported dictionary.
SaveDictionary(true, true);
ProcessLoad();
HideSettings();
document.getElementById("importFile").value = "";
} else {
var errorString = "File is missing:";
if (!tmpDicitonary.hasOwnProperty("name"))
errorString += " name";
if (!tmpDicitonary.hasOwnProperty("description"))
errorString += " description";
if (!tmpDicitonary.hasOwnProperty("words"))
errorString += " words";
if (!tmpDicitonary.hasOwnProperty("settings"))
errorString += " settings";
alert("Uploaded file is not compatible.\n\n" + errorString);
}
tmpDicitonary = null;
} else {
alert("Upload Failed");
}
reader = null;
}
} else {
alert("You must add a file to import.");
}
}
}
function WordIndex(word) {
for (var i = 0; i < currentDictionary.words.length; i++)
{
if ((!currentDictionary.settings.caseSensitive && currentDictionary.words[i].name.toLowerCase() == word.toLowerCase()) ||
(currentDictionary.settings.caseSensitive && currentDictionary.words[i].name == word)) {
return i;
}
}
return -1;
}
function WordAtIndexWasChanged(indexString, word, pronunciation, partOfSpeech, simpleDefinition, longDefinition) {
return (!currentDictionary.settings.caseSensitive && currentDictionary.words[parseInt(indexString)].name.toLowerCase() != word.toLowerCase()) ||
(currentDictionary.settings.caseSensitive && currentDictionary.words[parseInt(indexString)].name != word) ||
currentDictionary.words[parseInt(indexString)].pronunciation != pronunciation ||
currentDictionary.words[parseInt(indexString)].partOfSpeech != partOfSpeech ||
currentDictionary.words[parseInt(indexString)].simpleDefinition != simpleDefinition ||
currentDictionary.words[parseInt(indexString)].longDefinition != longDefinition;
}
function CheckForPartsOfSpeechChange() {
if (htmlEntities(document.getElementById("dictionaryPartsOfSpeechEdit").value) != currentDictionary.settings.partsOfSpeech) {
if (htmlEntities(document.getElementById("dictionaryPartsOfSpeechEdit").value) != "") {
currentDictionary.settings.partsOfSpeech = htmlEntities(document.getElementById("dictionaryPartsOfSpeechEdit").value);
SetPartsOfSpeech();
}
}
}

View File

@ -1,244 +0,0 @@
function ready(fn) {
if (document.readyState != 'loading'){
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
// Get Keycode based on key name
function keyCodeFor(keyName) {
if (keyName == "backspace") return 8;
else if (keyName == "tab") return 9;
else if (keyName == "ctrlEnter") return 10;
else if (keyName == "enter") return 13;
else if (keyName == "shift") return 16;
else if (keyName == "ctrl") return 17;
else if (keyName == "alt") return 18;
else if (keyName == "pausebreak") return 19;
else if (keyName == "capslock") return 20;
else if (keyName == "escape") return 27;
else if (keyName == "space") return 32;
else if (keyName == "pageup") return 33;
else if (keyName == "pagedown") return 34;
else if (keyName == "end") return 35;
else if (keyName == "home") return 36;
else if (keyName == "left") return 37;
else if (keyName == "up") return 38;
else if (keyName == "right") return 39;
else if (keyName == "down") return 40;
else if (keyName == "insert") return 45;
else if (keyName == "del") return 46;
else if (keyName == "zero") return 48;
else if (keyName == "one") return 49;
else if (keyName == "two") return 50;
else if (keyName == "three") return 51;
else if (keyName == "four") return 52;
else if (keyName == "five") return 53;
else if (keyName == "six") return 54;
else if (keyName == "seven") return 55;
else if (keyName == "eight") return 56;
else if (keyName == "nine") return 57;
else if (keyName == "a") return 65;
else if (keyName == "b") return 66;
else if (keyName == "c") return 67;
else if (keyName == "d") return 68;
else if (keyName == "e") return 69;
else if (keyName == "f") return 70;
else if (keyName == "g") return 71;
else if (keyName == "h") return 72;
else if (keyName == "i") return 73;
else if (keyName == "j") return 74;
else if (keyName == "k") return 75;
else if (keyName == "l") return 76;
else if (keyName == "m") return 77;
else if (keyName == "n") return 78;
else if (keyName == "o") return 79;
else if (keyName == "p") return 80;
else if (keyName == "q") return 81;
else if (keyName == "r") return 82;
else if (keyName == "s") return 83;
else if (keyName == "t") return 84;
else if (keyName == "u") return 85;
else if (keyName == "v") return 86;
else if (keyName == "w") return 87;
else if (keyName == "x") return 88;
else if (keyName == "y") return 89;
else if (keyName == "z") return 90;
else if (keyName == "leftwinkey") return 91;
else if (keyName == "rightwinkey") return 92;
else if (keyName == "selectkey") return 93;
else if (keyName == "numpad_0") return 96;
else if (keyName == "numpad_1") return 97;
else if (keyName == "numpad_2") return 98;
else if (keyName == "numpad_3") return 99;
else if (keyName == "numpad_4") return 100;
else if (keyName == "numpad_5") return 101;
else if (keyName == "numpad_6") return 102;
else if (keyName == "numpad_7") return 103;
else if (keyName == "numpad_8") return 104;
else if (keyName == "numpad_9") return 105;
else if (keyName == "numpad_asterisk") return 106;
else if (keyName == "numpad_plus") return 107;
else if (keyName == "numpad_dash") return 109;
else if (keyName == "numpad_period") return 110;
else if (keyName == "numpad_slash") return 111;
else if (keyName == "f1") return 112;
else if (keyName == "f2") return 113;
else if (keyName == "f3") return 114;
else if (keyName == "f4") return 115;
else if (keyName == "f5") return 116;
else if (keyName == "f6") return 117;
else if (keyName == "f7") return 118;
else if (keyName == "f8") return 119;
else if (keyName == "f9") return 120;
else if (keyName == "f10") return 121;
else if (keyName == "f11") return 122;
else if (keyName == "f12") return 123;
else if (keyName == "numlock") return 144;
else if (keyName == "scrolllock") return 145;
else if (keyName == "semicolon") return 186;
else if (keyName == "equal") return 187;
else if (keyName == "comma") return 188;
else if (keyName == "dash") return 189;
else if (keyName == "period") return 190;
else if (keyName == "slash") return 191;
else if (keyName == "grave") return 192;
else if (keyName == "openbracket") return 219;
else if (keyName == "backslash") return 220;
else if (keyName == "closebraket") return 221;
else if (keyName == "quote") return 222;
else return false;
}
function getInputSelection(el) {
// Retrieved from http://stackoverflow.com/a/4207763
var start = 0, end = 0, normalizedValue, range,
textInputRange, len, endRange;
el.focus();
if (typeof el.selectionStart == "number" && typeof el.selectionEnd == "number") {
start = el.selectionStart;
end = el.selectionEnd;
} else {
range = document.selection.createRange();
if (range && range.parentElement() == el) {
len = el.value.length;
normalizedValue = el.value.replace(/\r\n/g, "\n");
// Create a working TextRange that lives only in the input
textInputRange = el.createTextRange();
textInputRange.moveToBookmark(range.getBookmark());
// Check if the start and end of the selection are at the very end
// of the input, since moveStart/moveEnd doesn't return what we want
// in those cases
endRange = el.createTextRange();
endRange.collapse(false);
if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
start = end = len;
} else {
start = -textInputRange.moveStart("character", -len);
start += normalizedValue.slice(0, start).split("\n").length - 1;
if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) {
end = len;
} else {
end = -textInputRange.moveEnd("character", -len);
end += normalizedValue.slice(0, end).split("\n").length - 1;
}
}
}
}
return {
start: start,
end: end
};
}
function setSelectionRange(input, selectionStart, selectionEnd) {
// Retrieved from http://stackoverflow.com/a/17858641/3508346
if (input.setSelectionRange) {
input.focus();
input.setSelectionRange(selectionStart, selectionEnd);
}
else if (input.createTextRange) {
var range = input.createTextRange();
range.collapse(true);
range.moveEnd('character', selectionEnd);
range.moveStart('character', selectionStart);
range.select();
}
}
function SaveScroll() {
var doc = document.documentElement;
var left = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0);
var top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
savedScroll.x = left;
savedScroll.y = top;
}
function htmlEntities(string) {
return String(string).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;').replace(/\\/g, '&#92;').replace(/\n/g, '<br>');
}
function htmlEntitiesParse(string) {
return String(string).replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&#92;/g, '\\').replace(/<br>/g, '\n');
}
function stripHtmlEntities(string) {
// This is for the export name.
return String(string).replace(/&amp;/g, '').replace(/&lt;/g, '').replace(/&gt;/g, '').replace(/&quot;/g, '').replace(/&apos;/g, "").replace(/&#92;/g, '').replace(/<br>/g, '');
}
function htmlEntitiesParseForSearchEntry(string) {
return String(string).replace(/"/g, '%%%%').replace(/'/g, "````");
}
function htmlEntitiesParseForSearch(string) {
return String(string).replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '%%%%').replace(/&apos;/g, "````");
}
function regexParseForSearch(string) {
return String(string).replace(/([\[\\\^\$\.\|\?\*\+\(\)\{\}\]])/g, "\\$1");
}
function dynamicSort(propertiesArray) {
/* Retrieved from http://stackoverflow.com/a/30446887/3508346
Usage: theArray.sort(dynamicSort(['propertyAscending', '-propertyDescending']));*/
return function (a, b) {
return propertiesArray
.map(function (o) {
var dir = 1;
if (o[0] === '-') {
dir = -1;
o=o.substring(1);
}
if (removeDiacritics(a[o]) > removeDiacritics(b[o])) return dir;
if (removeDiacritics(a[o]) < removeDiacritics(b[o])) return -(dir);
return 0;
})
.reduce(function firstNonZeroValue (p,n) {
return p ? p : n;
}, 0);
};
}
function download(filename, text) {
/* Retrieved from http://stackoverflow.com/a/18197341/3508346
Usage: download('test.txt', 'Hello world!');*/
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
function ready(e){"loading"!=document.readyState?e():document.addEventListener("DOMContentLoaded",e)}function keyCodeFor(e){return"backspace"==e?8:"tab"==e?9:"ctrlEnter"==e?10:"enter"==e?13:"shift"==e?16:"ctrl"==e?17:"alt"==e?18:"pausebreak"==e?19:"capslock"==e?20:"escape"==e?27:"space"==e?32:"pageup"==e?33:"pagedown"==e?34:"end"==e?35:"home"==e?36:"left"==e?37:"up"==e?38:"right"==e?39:"down"==e?40:"insert"==e?45:"del"==e?46:"zero"==e?48:"one"==e?49:"two"==e?50:"three"==e?51:"four"==e?52:"five"==e?53:"six"==e?54:"seven"==e?55:"eight"==e?56:"nine"==e?57:"a"==e?65:"b"==e?66:"c"==e?67:"d"==e?68:"e"==e?69:"f"==e?70:"g"==e?71:"h"==e?72:"i"==e?73:"j"==e?74:"k"==e?75:"l"==e?76:"m"==e?77:"n"==e?78:"o"==e?79:"p"==e?80:"q"==e?81:"r"==e?82:"s"==e?83:"t"==e?84:"u"==e?85:"v"==e?86:"w"==e?87:"x"==e?88:"y"==e?89:"z"==e?90:"leftwinkey"==e?91:"rightwinkey"==e?92:"selectkey"==e?93:"numpad_0"==e?96:"numpad_1"==e?97:"numpad_2"==e?98:"numpad_3"==e?99:"numpad_4"==e?100:"numpad_5"==e?101:"numpad_6"==e?102:"numpad_7"==e?103:"numpad_8"==e?104:"numpad_9"==e?105:"numpad_asterisk"==e?106:"numpad_plus"==e?107:"numpad_dash"==e?109:"numpad_period"==e?110:"numpad_slash"==e?111:"f1"==e?112:"f2"==e?113:"f3"==e?114:"f4"==e?115:"f5"==e?116:"f6"==e?117:"f7"==e?118:"f8"==e?119:"f9"==e?120:"f10"==e?121:"f11"==e?122:"f12"==e?123:"numlock"==e?144:"scrolllock"==e?145:"semicolon"==e?186:"equal"==e?187:"comma"==e?188:"dash"==e?189:"period"==e?190:"slash"==e?191:"grave"==e?192:"openbracket"==e?219:"backslash"==e?220:"closebraket"==e?221:"quote"==e?222:!1}function getInputSelection(e){var t,n,r,a,c,o=0,l=0;return e.focus(),"number"==typeof e.selectionStart&&"number"==typeof e.selectionEnd?(o=e.selectionStart,l=e.selectionEnd):(n=document.selection.createRange(),n&&n.parentElement()==e&&(a=e.value.length,t=e.value.replace(/\r\n/g,"\n"),r=e.createTextRange(),r.moveToBookmark(n.getBookmark()),c=e.createTextRange(),c.collapse(!1),r.compareEndPoints("StartToEnd",c)>-1?o=l=a:(o=-r.moveStart("character",-a),o+=t.slice(0,o).split("\n").length-1,r.compareEndPoints("EndToEnd",c)>-1?l=a:(l=-r.moveEnd("character",-a),l+=t.slice(0,l).split("\n").length-1)))),{start:o,end:l}}function setSelectionRange(e,t,n){if(e.setSelectionRange)e.focus(),e.setSelectionRange(t,n);else if(e.createTextRange){var r=e.createTextRange();r.collapse(!0),r.moveEnd("character",n),r.moveStart("character",t),r.select()}}function SaveScroll(){var e=document.documentElement,t=(window.pageXOffset||e.scrollLeft)-(e.clientLeft||0),n=(window.pageYOffset||e.scrollTop)-(e.clientTop||0);savedScroll.x=t,savedScroll.y=n}function htmlEntities(e){return String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&apos;").replace(/\\/g,"&#92;").replace(/\n/g,"<br>")}function htmlEntitiesParse(e){return String(e).replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&quot;/g,'"').replace(/&apos;/g,"'").replace(/&#92;/g,"\\").replace(/<br>/g,"\n")}function stripHtmlEntities(e){return String(e).replace(/&amp;/g,"").replace(/&lt;/g,"").replace(/&gt;/g,"").replace(/&quot;/g,"").replace(/&apos;/g,"").replace(/&#92;/g,"").replace(/<br>/g,"")}function htmlEntitiesParseForSearchEntry(e){return String(e).replace(/"/g,"%%%%").replace(/'/g,"````")}function htmlEntitiesParseForSearch(e){return String(e).replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&quot;/g,"%%%%").replace(/&apos;/g,"````")}function regexParseForSearch(e){return String(e).replace(/([\[\\\^\$\.\|\?\*\+\(\)\{\}\]])/g,"\\$1")}function dynamicSort(e){return function(t,n){return e.map(function(e){var r=1;return"-"===e[0]&&(r=-1,e=e.substring(1)),removeDiacritics(t[e])>removeDiacritics(n[e])?r:removeDiacritics(t[e])<removeDiacritics(n[e])?-r:0}).reduce(function(e,t){return e?e:t},0)}}function download(e,t){var n=document.createElement("a");n.setAttribute("href","data:text/plain;charset=utf-8,"+encodeURIComponent(t)),n.setAttribute("download",e),n.style.display="none",document.body.appendChild(n),n.click(),document.body.removeChild(n)}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
function IsValidPublicDicitonary(){return"string"!=typeof publicDictionary}function ShowPublicDictionary(){if(IsValidPublicDicitonary()){var e=document.getElementById("wordFilter").value,i=[],t=htmlEntitiesParseForSearchEntry(document.getElementById("searchBox").value),n=document.getElementById("searchOptionWord").checked,r=document.getElementById("searchOptionSimple").checked,c=document.getElementById("searchOptionLong").checked,o=!document.getElementById("searchCaseSensitive").checked,a=document.getElementById("searchIgnoreDiacritics").checked;if(""!=t&&(n||r||c)){var d=[],s=htmlEntitiesParseForSearch(JSON.stringify(publicDictionary));o&&(t=t.toLowerCase()),a&&(t=removeDiacritics(t),s=removeDiacritics(s)),n&&d.push("contains("+(o?"name":'translate(name, "", "")')+', "'+t+'")'),r&&d.push("contains("+(o?"simpleDefinition":'translate(simpleDefinition, "", "")')+', "'+t+'")'),c&&d.push("contains("+(o?"longDefinition":'translate(longDefinition, "", "")')+', "'+t+'")');var l=JSON.parse(s);i=JSON.search(l,"//words["+d.join(" or ")+"]/wordId")}var m=document.getElementById("dictionaryName");m.innerHTML=htmlEntitiesParse(publicDictionary.name)+" Dictionary";var p=document.getElementById("dictionaryBy");p.innerHTML="created by "+htmlEntitiesParse(publicDictionary.createdBy);var u=document.getElementById("incompleteNotice");publicDictionary.settings.isComplete||(u.innerHTML="<em>Note: This dictionary is not yet complete and is likely to change.</em>");var y=document.getElementById("dictionaryDescription");y.innerHTML=marked(htmlEntitiesParse(publicDictionary.description));var h=document.getElementById("theDictionary"),D="",g=0;if(publicDictionary.words.length>0)for(var w=0;w<publicDictionary.words.length;w++)(""==e||""!=e&&publicDictionary.words[w].partOfSpeech==e)&&(""==t||""!=t&&(n||r||c)&&i.indexOf(publicDictionary.words[w].wordId)>=0)&&(publicDictionary.words[w].hasOwnProperty("pronunciation")||(publicDictionary.words[w].pronunciation=""),publicDictionary.words[w].hasOwnProperty("wordId")||(publicDictionary.words[w].wordId=w+1),D+=PublicDictionaryEntry(w),g++);else D="There are no entries in the dictionary.";h.innerHTML=D,ShowFilterWordCount(g)}else document.getElementById("dictionaryContainer").innerHTML=publicDictionary}function PublicDictionaryEntry(e){var i="<entry><a name='"+publicDictionary.words[e].wordId+"'></a><a href='#"+publicDictionary.words[e].wordId+"' class='wordLink clickable'>&#x1f517;</a>",t=regexParseForSearch(document.getElementById("searchBox").value),n=document.getElementById("searchOptionWord").checked,r=document.getElementById("searchOptionSimple").checked,c=document.getElementById("searchOptionLong").checked,o=!document.getElementById("searchCaseSensitive").checked,a=document.getElementById("searchIgnoreDiacritics").checked,d=new RegExp("("+(a?removeDiacritics(t)+"|"+t:t)+")","g"+(o?"i":""));return i+="<word>",i+=""!=t&&n?htmlEntitiesParse(publicDictionary.words[e].name).replace(d,"<searchTerm>$1</searchterm>"):publicDictionary.words[e].name,i+="</word>",""!=publicDictionary.words[e].pronunciation&&(i+="<pronunciation>",i+=marked(htmlEntitiesParse(publicDictionary.words[e].pronunciation)).replace("<p>","").replace("</p>",""),i+="</pronunciation>"),""!=publicDictionary.words[e].partOfSpeech&&(i+="<partofspeech>",i+=publicDictionary.words[e].partOfSpeech,i+="</partofspeech>"),i+="<br>",""!=publicDictionary.words[e].simpleDefinition&&(i+="<simpledefinition>",i+=""!=t&&r?htmlEntitiesParse(publicDictionary.words[e].simpleDefinition).replace(d,"<searchTerm>$1</searchterm>"):publicDictionary.words[e].simpleDefinition,i+="</simpledefinition>"),""!=publicDictionary.words[e].longDefinition&&(i+="<longdefinition>",i+=""!=t&&c?marked(htmlEntitiesParse(publicDictionary.words[e].longDefinition).replace(d,"<searchTerm>$1</searchterm>")):marked(htmlEntitiesParse(publicDictionary.words[e].longDefinition)),i+="</longdefinition>"),i+="</entry>"}function SetPublicPartsOfSpeech(){for(var e=document.getElementById("wordFilter"),i=htmlEntitiesParse(publicDictionary.settings.partsOfSpeech).trim().split(","),t=0;t<i.length;t++){var n=document.createElement("option");n.appendChild(document.createTextNode(i[t].trim())),n.value=i[t].trim(),e.appendChild(n)}}

View File

@ -1,16 +0,0 @@
/*
Retrieved from http://stackoverflow.com/a/18391901/3508346
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
function removeDiacritics(e){return e.replace(/[^\u0000-\u007E]/g,function(e){return diacriticsMap[e]||e})}for(var defaultDiacriticsRemovalap=[{base:"A",letters:"AⒶÀÁÂẦẤẪẨÃĀĂẰẮẴẲȦǠÄǞẢÅǺǍȀȂẠẬẶḀĄȺⱯ"},{base:"AA",letters:"Ꜳ"},{base:"AE",letters:"ÆǼǢ"},{base:"AO",letters:"Ꜵ"},{base:"AU",letters:"Ꜷ"},{base:"AV",letters:"ꜸꜺ"},{base:"AY",letters:"Ꜽ"},{base:"B",letters:"BⒷḂḄḆɃƂƁ"},{base:"C",letters:"CⒸĆĈĊČÇḈƇȻꜾ"},{base:"D",letters:"DⒹḊĎḌḐḒḎĐƋƊƉꝹ"},{base:"DZ",letters:"DZDŽ"},{base:"Dz",letters:"DzDž"},{base:"E",letters:"EⒺÈÉÊỀẾỄỂẼĒḔḖĔĖËẺĚȄȆẸỆȨḜĘḘḚƐƎ"},{base:"F",letters:"FⒻḞƑꝻ"},{base:"G",letters:"GⒼǴĜḠĞĠǦĢǤƓꞠꝽꝾ"},{base:"H",letters:"HⒽĤḢḦȞḤḨḪĦⱧⱵꞍ"},{base:"I",letters:"IⒾÌÍÎĨĪĬİÏḮỈǏȈȊỊĮḬƗ"},{base:"J",letters:"JⒿĴɈ"},{base:"K",letters:"KⓀḰǨḲĶḴƘⱩꝀꝂꝄꞢ"},{base:"L",letters:"LⓁĿĹĽḶḸĻḼḺŁȽⱢⱠꝈꝆꞀ"},{base:"LJ",letters:"LJ"},{base:"Lj",letters:"Lj"},{base:"M",letters:"MⓂḾṀṂⱮƜ"},{base:"N",letters:"NⓃǸŃÑṄŇṆŅṊṈȠƝꞐꞤ"},{base:"NJ",letters:"NJ"},{base:"Nj",letters:"Nj"},{base:"O",letters:"OⓄÒÓÔỒỐỖỔÕṌȬṎŌṐṒŎȮȰÖȪỎŐǑȌȎƠỜỚỠỞỢỌỘǪǬØǾƆƟꝊꝌ"},{base:"OI",letters:"Ƣ"},{base:"OO",letters:"Ꝏ"},{base:"OU",letters:"Ȣ"},{base:"OE",letters:"ŒŒ"},{base:"oe",letters:"œœ"},{base:"P",letters:"PⓅṔṖƤⱣꝐꝒꝔ"},{base:"Q",letters:"QⓆꝖꝘɊ"},{base:"R",letters:"RⓇŔṘŘȐȒṚṜŖṞɌⱤꞦꞂ"},{base:"S",letters:"SⓈẞŚṤŜṠŠṦṢṨȘŞⱾꞨꞄ"},{base:"T",letters:"TⓉṪŤṬȚŢṰṮŦƬƮȾꞆ"},{base:"TZ",letters:"Ꜩ"},{base:"U",letters:"UⓊÙÚÛŨṸŪṺŬÜǛǗǕǙỦŮŰǓȔȖƯỪỨỮỬỰỤṲŲṶṴɄ"},{base:"V",letters:"VⓋṼṾƲꝞɅ"},{base:"VY",letters:"Ꝡ"},{base:"W",letters:"WⓌẀẂŴẆẄẈⱲ"},{base:"X",letters:"XⓍẊẌ"},{base:"Y",letters:"YⓎỲÝŶỸȲẎŸỶỴƳɎỾ"},{base:"Z",letters:"ZⓏŹẐŻŽẒẔƵȤⱿⱫꝢ"},{base:"a",letters:"aⓐẚàáâầấẫẩãāăằắẵẳȧǡäǟảåǻǎȁȃạậặḁąⱥɐ"},{base:"aa",letters:"ꜳ"},{base:"ae",letters:"æǽǣ"},{base:"ao",letters:"ꜵ"},{base:"au",letters:"ꜷ"},{base:"av",letters:"ꜹꜻ"},{base:"ay",letters:"ꜽ"},{base:"b",letters:"bⓑḃḅḇƀƃɓ"},{base:"c",letters:"cⓒćĉċčçḉƈȼꜿↄ"},{base:"d",letters:"dⓓḋďḍḑḓḏđƌɖɗꝺ"},{base:"dz",letters:"dzdž"},{base:"e",letters:"eⓔèéêềếễểẽēḕḗĕėëẻěȅȇẹệȩḝęḙḛɇɛǝ"},{base:"f",letters:"fⓕḟƒꝼ"},{base:"g",letters:"gⓖǵĝḡğġǧģǥɠꞡᵹꝿ"},{base:"h",letters:"hⓗĥḣḧȟḥḩḫẖħⱨⱶɥ"},{base:"hv",letters:"ƕ"},{base:"i",letters:"iⓘìíîĩīĭïḯỉǐȉȋịįḭɨı"},{base:"j",letters:"jⓙĵǰɉ"},{base:"k",letters:"kⓚḱǩḳķḵƙⱪꝁꝃꝅꞣ"},{base:"l",letters:"lⓛŀĺľḷḹļḽḻſłƚɫⱡꝉꞁꝇ"},{base:"lj",letters:"lj"},{base:"m",letters:"mⓜḿṁṃɱɯ"},{base:"n",letters:"nⓝǹńñṅňṇņṋṉƞɲʼnꞑꞥ"},{base:"nj",letters:"nj"},{base:"o",letters:"oⓞòóôồốỗổõṍȭṏōṑṓŏȯȱöȫỏőǒȍȏơờớỡởợọộǫǭøǿɔꝋꝍɵ"},{base:"oi",letters:"ƣ"},{base:"ou",letters:"ȣ"},{base:"oo",letters:"ꝏ"},{base:"p",letters:"pⓟṕṗƥᵽꝑꝓꝕ"},{base:"q",letters:"qⓠɋꝗꝙ"},{base:"r",letters:"rⓡŕṙřȑȓṛṝŗṟɍɽꝛꞧꞃ"},{base:"s",letters:"sⓢßśṥŝṡšṧṣṩșşȿꞩꞅẛ"},{base:"t",letters:"tⓣṫẗťṭțţṱṯŧƭʈⱦꞇ"},{base:"tz",letters:"ꜩ"},{base:"u",letters:"uⓤùúûũṹūṻŭüǜǘǖǚủůűǔȕȗưừứữửựụṳųṷṵʉ"},{base:"v",letters:"vⓥṽṿʋꝟʌ"},{base:"vy",letters:"ꝡ"},{base:"w",letters:"wⓦẁẃŵẇẅẘẉⱳ"},{base:"x",letters:"xⓧẋẍ"},{base:"y",letters:"yⓨỳýŷỹȳẏÿỷẙỵƴɏỿ"},{base:"z",letters:"zⓩźẑżžẓẕƶȥɀⱬꝣ"}],diacriticsMap={},i=0;i<defaultDiacriticsRemovalap.length;i++)for(var letters=defaultDiacriticsRemovalap[i].letters,j=0;j<letters.length;j++)diacriticsMap[letters[j]]=defaultDiacriticsRemovalap[i].base;

File diff suppressed because one or more lines are too long

View File

@ -1,158 +0,0 @@
function IsValidPublicDicitonary() {
return typeof publicDictionary !== 'string';
}
function ShowPublicDictionary() {
if (IsValidPublicDicitonary()) {
var filter = document.getElementById("wordFilter").value;
var searchResults = [];
var search = htmlEntitiesParseForSearchEntry(document.getElementById("searchBox").value);
var searchByWord = document.getElementById("searchOptionWord").checked;
var searchBySimple = document.getElementById("searchOptionSimple").checked;
var searchByLong = document.getElementById("searchOptionLong").checked;
var searchIgnoreCase = !document.getElementById("searchCaseSensitive").checked; //It's easier to negate case here instead of negating it every use since ignore case is default.
var searchIgnoreDiacritics = document.getElementById("searchIgnoreDiacritics").checked;
if (search != "" && (searchByWord || searchBySimple || searchByLong)) {
var xpath = [];
var searchDictionaryJSON = htmlEntitiesParseForSearch(JSON.stringify(publicDictionary));
if (searchIgnoreCase) {
search = search.toLowerCase();
//searchDictionaryJSON = searchDictionaryJSON.toLowerCase();
}
if (searchIgnoreDiacritics) {
search = removeDiacritics(search);
searchDictionaryJSON = removeDiacritics(searchDictionaryJSON);
}
if (searchByWord) {
xpath.push('contains('+ ((searchIgnoreCase) ? 'name' : 'translate(name, "", "")') +', "'+ search +'")');
}
if (searchBySimple) {
xpath.push('contains('+ ((searchIgnoreCase) ? 'simpleDefinition' : 'translate(simpleDefinition, "", "")') +', "'+ search +'")');
}
if (searchByLong) {
xpath.push('contains('+ ((searchIgnoreCase) ? 'longDefinition' : 'translate(longDefinition, "", "")') +', "'+ search +'")');
}
var searchDictionary = JSON.parse(searchDictionaryJSON);
searchResults = JSON.search(searchDictionary, '//words['+ xpath.join(' or ') +']/wordId');
}
var dictionaryNameArea = document.getElementById("dictionaryName");
dictionaryNameArea.innerHTML = htmlEntitiesParse(publicDictionary.name) + " Dictionary";
var dictionaryByArea = document.getElementById("dictionaryBy");
dictionaryByArea.innerHTML = "created by " + htmlEntitiesParse(publicDictionary.createdBy);
var dictionaryIncompleteArea = document.getElementById("incompleteNotice");
if (!publicDictionary.settings.isComplete) {
dictionaryIncompleteArea.innerHTML = "<em>Note: This dictionary is not yet complete and is likely to change.</em>";
}
var dictionaryDescriptionArea = document.getElementById("dictionaryDescription");
dictionaryDescriptionArea.innerHTML = marked(htmlEntitiesParse(publicDictionary.description));
var dictionaryArea = document.getElementById("theDictionary");
var dictionaryText = "";
var numberOfWordsDisplayed = 0;
if (publicDictionary.words.length > 0) {
for (var i = 0; i < publicDictionary.words.length; i++) {
if (filter == "" || (filter != "" && publicDictionary.words[i].partOfSpeech == filter)) {
if (search == "" || (search != "" && (searchByWord || searchBySimple || searchByLong) && searchResults.indexOf(publicDictionary.words[i].wordId) >= 0)) {
if (!publicDictionary.words[i].hasOwnProperty("pronunciation")) {
publicDictionary.words[i].pronunciation = ""; //Account for new property
}
if (!publicDictionary.words[i].hasOwnProperty("wordId")) {
publicDictionary.words[i].wordId = i + 1; //Account for new property
}
dictionaryText += PublicDictionaryEntry(i);
numberOfWordsDisplayed++;
}
}
}
} else {
dictionaryText = "There are no entries in the dictionary."
}
dictionaryArea.innerHTML = dictionaryText;
ShowFilterWordCount(numberOfWordsDisplayed);
} else {
document.getElementById("dictionaryContainer").innerHTML = publicDictionary;
}
}
function PublicDictionaryEntry(itemIndex) {
var entryText = "<entry><a name='" + publicDictionary.words[itemIndex].wordId + "'></a><a href='#" + publicDictionary.words[itemIndex].wordId + "' class='wordLink clickable'>&#x1f517;</a>";
var searchTerm = regexParseForSearch(document.getElementById("searchBox").value);
var searchByWord = document.getElementById("searchOptionWord").checked;
var searchBySimple = document.getElementById("searchOptionSimple").checked;
var searchByLong = document.getElementById("searchOptionLong").checked;
var searchIgnoreCase = !document.getElementById("searchCaseSensitive").checked; //It's easier to negate case here instead of negating it every use since ignore case is default.
var searchIgnoreDiacritics = document.getElementById("searchIgnoreDiacritics").checked;
var searchRegEx = new RegExp("(" + ((searchIgnoreDiacritics) ? removeDiacritics(searchTerm) + "|" + searchTerm : searchTerm) + ")", "g" + ((searchIgnoreCase) ? "i" : ""));
entryText += "<word>";
if (searchTerm != "" && searchByWord) {
entryText += htmlEntitiesParse(publicDictionary.words[itemIndex].name).replace(searchRegEx, "<searchTerm>$1</searchterm>");
} else {
entryText += publicDictionary.words[itemIndex].name;
}
entryText += "</word>";
if (publicDictionary.words[itemIndex].pronunciation != "") {
entryText += "<pronunciation>";
entryText += marked(htmlEntitiesParse(publicDictionary.words[itemIndex].pronunciation)).replace("<p>","").replace("</p>","");
entryText += "</pronunciation>";
}
if (publicDictionary.words[itemIndex].partOfSpeech != "") {
entryText += "<partofspeech>";
entryText += publicDictionary.words[itemIndex].partOfSpeech;
entryText += "</partofspeech>";
}
entryText += "<br>";
if (publicDictionary.words[itemIndex].simpleDefinition != "") {
entryText += "<simpledefinition>";
if (searchTerm != "" && searchBySimple) {
entryText += htmlEntitiesParse(publicDictionary.words[itemIndex].simpleDefinition).replace(searchRegEx, "<searchTerm>$1</searchterm>");
} else {
entryText += publicDictionary.words[itemIndex].simpleDefinition;
}
entryText += "</simpledefinition>";
}
if (publicDictionary.words[itemIndex].longDefinition != "") {
entryText += "<longdefinition>";
if (searchTerm != "" && searchByLong) {
entryText += marked(htmlEntitiesParse(publicDictionary.words[itemIndex].longDefinition).replace(searchRegEx, "<searchTerm>$1</searchterm>"));
} else {
entryText += marked(htmlEntitiesParse(publicDictionary.words[itemIndex].longDefinition));
}
entryText += "</longdefinition>";
}
entryText += "</entry>";
return entryText;
}
function SetPublicPartsOfSpeech () {
var wordFilterSelect = document.getElementById("wordFilter");
var newPartsOfSpeech = htmlEntitiesParse(publicDictionary.settings.partsOfSpeech).trim().split(",");
for (var j = 0; j < newPartsOfSpeech.length; j++) {
var wordFilterOption = document.createElement('option');
wordFilterOption.appendChild(document.createTextNode(newPartsOfSpeech[j].trim()));
wordFilterOption.value = newPartsOfSpeech[j].trim();
wordFilterSelect.appendChild(wordFilterOption);
}
}

View File

@ -1,118 +0,0 @@
/*
Retrieved from http://stackoverflow.com/a/18391901/3508346
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var defaultDiacriticsRemovalap = [
{'base':'A', 'letters':'\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F'},
{'base':'AA','letters':'\uA732'},
{'base':'AE','letters':'\u00C6\u01FC\u01E2'},
{'base':'AO','letters':'\uA734'},
{'base':'AU','letters':'\uA736'},
{'base':'AV','letters':'\uA738\uA73A'},
{'base':'AY','letters':'\uA73C'},
{'base':'B', 'letters':'\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181'},
{'base':'C', 'letters':'\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E'},
{'base':'D', 'letters':'\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779'},
{'base':'DZ','letters':'\u01F1\u01C4'},
{'base':'Dz','letters':'\u01F2\u01C5'},
{'base':'E', 'letters':'\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E'},
{'base':'F', 'letters':'\u0046\u24BB\uFF26\u1E1E\u0191\uA77B'},
{'base':'G', 'letters':'\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E'},
{'base':'H', 'letters':'\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D'},
{'base':'I', 'letters':'\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197'},
{'base':'J', 'letters':'\u004A\u24BF\uFF2A\u0134\u0248'},
{'base':'K', 'letters':'\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2'},
{'base':'L', 'letters':'\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780'},
{'base':'LJ','letters':'\u01C7'},
{'base':'Lj','letters':'\u01C8'},
{'base':'M', 'letters':'\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C'},
{'base':'N', 'letters':'\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4'},
{'base':'NJ','letters':'\u01CA'},
{'base':'Nj','letters':'\u01CB'},
{'base':'O', 'letters':'\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C'},
{'base':'OI','letters':'\u01A2'},
{'base':'OO','letters':'\uA74E'},
{'base':'OU','letters':'\u0222'},
{'base':'OE','letters':'\u008C\u0152'},
{'base':'oe','letters':'\u009C\u0153'},
{'base':'P', 'letters':'\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754'},
{'base':'Q', 'letters':'\u0051\u24C6\uFF31\uA756\uA758\u024A'},
{'base':'R', 'letters':'\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782'},
{'base':'S', 'letters':'\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784'},
{'base':'T', 'letters':'\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786'},
{'base':'TZ','letters':'\uA728'},
{'base':'U', 'letters':'\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244'},
{'base':'V', 'letters':'\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245'},
{'base':'VY','letters':'\uA760'},
{'base':'W', 'letters':'\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72'},
{'base':'X', 'letters':'\u0058\u24CD\uFF38\u1E8A\u1E8C'},
{'base':'Y', 'letters':'\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE'},
{'base':'Z', 'letters':'\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762'},
{'base':'a', 'letters':'\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250'},
{'base':'aa','letters':'\uA733'},
{'base':'ae','letters':'\u00E6\u01FD\u01E3'},
{'base':'ao','letters':'\uA735'},
{'base':'au','letters':'\uA737'},
{'base':'av','letters':'\uA739\uA73B'},
{'base':'ay','letters':'\uA73D'},
{'base':'b', 'letters':'\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253'},
{'base':'c', 'letters':'\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184'},
{'base':'d', 'letters':'\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A'},
{'base':'dz','letters':'\u01F3\u01C6'},
{'base':'e', 'letters':'\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD'},
{'base':'f', 'letters':'\u0066\u24D5\uFF46\u1E1F\u0192\uA77C'},
{'base':'g', 'letters':'\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F'},
{'base':'h', 'letters':'\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265'},
{'base':'hv','letters':'\u0195'},
{'base':'i', 'letters':'\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131'},
{'base':'j', 'letters':'\u006A\u24D9\uFF4A\u0135\u01F0\u0249'},
{'base':'k', 'letters':'\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3'},
{'base':'l', 'letters':'\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747'},
{'base':'lj','letters':'\u01C9'},
{'base':'m', 'letters':'\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F'},
{'base':'n', 'letters':'\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5'},
{'base':'nj','letters':'\u01CC'},
{'base':'o', 'letters':'\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275'},
{'base':'oi','letters':'\u01A3'},
{'base':'ou','letters':'\u0223'},
{'base':'oo','letters':'\uA74F'},
{'base':'p','letters':'\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755'},
{'base':'q','letters':'\u0071\u24E0\uFF51\u024B\uA757\uA759'},
{'base':'r','letters':'\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783'},
{'base':'s','letters':'\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B'},
{'base':'t','letters':'\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787'},
{'base':'tz','letters':'\uA729'},
{'base':'u','letters': '\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289'},
{'base':'v','letters':'\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C'},
{'base':'vy','letters':'\uA761'},
{'base':'w','letters':'\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73'},
{'base':'x','letters':'\u0078\u24E7\uFF58\u1E8B\u1E8D'},
{'base':'y','letters':'\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF'},
{'base':'z','letters':'\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763'}
];
var diacriticsMap = {};
for (var i=0; i < defaultDiacriticsRemovalap.length; i++){
var letters = defaultDiacriticsRemovalap[i].letters;
for (var j=0; j < letters.length ; j++){
diacriticsMap[letters[j]] = defaultDiacriticsRemovalap[i].base;
}
}
// "what?" version ... http://jsperf.com/diacritics/12
function removeDiacritics(str) {
return str.replace(/[^\u0000-\u007E]/g, function(a){
return diacriticsMap[a] || a;
});
}

665
js/ui.js
View File

@ -1,665 +0,0 @@
function Initialize() {
LoadDictionary();
ClearForm();
LoadUserDictionaries();
GetTextFile("README.md", "aboutText", true);
GetTextFile("TERMS.md", "termsText", true);
GetTextFile("PRIVACY.md", "privacyText", true);
GetTextFile("LOGIN.form", "loginForm", false);
GetTextFile("FORGOT.form", "forgotForm", false);
SetKeyboardShortcuts();
}
function SetKeyboardShortcuts() {
document.addEventListener("keydown", function(e) {
var keyCode = (e.which ? e.which : e.keyCode);
if (keyCode == keyCodeFor("escape")) {
if (document.getElementById("infoScreen").style.display == "block") {
HideInfo();
}
else if (document.getElementById("fullScreenTextboxScreen").style.display == "block") {
HideFullScreenTextbox();
}
else if (document.getElementById("settingsScreen").style.display == "block") {
HideSettings();
}
else if (document.getElementById("accountSettingsScreen") && document.getElementById("accountSettingsScreen").style.display == "block") {
HideAccountSettings();
}
}
else if (e.ctrlKey) {
// Only allow shortcuts if not currently using fullscreen textbox
if (document.getElementById("fullScreenTextboxScreen").style.display == "none") {
if (keyCode == keyCodeFor("m")) {
if (document.activeElement.id == "longDefinition") {
e.preventDefault();
ShowFullScreenTextbox('longDefinition', 'Explanation/Long Definition');
}
else if (document.activeElement.id == "dictionaryDescriptionEdit") {
e.preventDefault();
ShowFullScreenTextbox('dictionaryDescriptionEdit', 'Dictionary Details');
}
else if (document.activeElement.id == "fullScreenTextbox") {
e.preventDefault();
HideFullScreenTextbox();
}
}
else if (keyCode == keyCodeFor("u")) {
e.preventDefault();
ToggleWordFormLock();
}
else if (keyCode == keyCodeFor("d")) {
e.preventDefault();
ToggleDescription();
}
else if ((e.shiftKey && keyCode == keyCodeFor("s")) || keyCode == keyCodeFor("e")) {
e.preventDefault();
ExportDictionary();
}
else if (keyCode == keyCodeFor("s")) {
e.preventDefault();
//ToggleSearchFilter();
var searchFilterToggle = document.getElementById("searchFilterToggle");
var searchFilterArea = document.getElementById("searchFilterArea");
if (searchFilterArea.style.display == "none") {
searchFilterArea.style.display = "block";
searchFilterToggle.innerHTML = "Hide Search/Filter Options";
}
document.getElementById("searchBox").focus();
}
else if (keyCode == keyCodeFor("h")) {
e.preventDefault();
ShowInfo('aboutText');
}
}
else { //If the fullscreen editor *is* open, just prevent the others for consistent behavior.
if (keyCode == keyCodeFor("m")) {
e.preventDefault();
HideFullScreenTextbox();
}
else if (keyCode == keyCodeFor("u")) {
e.preventDefault();
}
else if (keyCode == keyCodeFor("d")) {
e.preventDefault();
}
else if ((e.shiftKey && keyCode == keyCodeFor("s")) || keyCode == keyCodeFor("e")) {
e.preventDefault();
}
else if (keyCode == keyCodeFor("s")) {
e.preventDefault();
}
else if (keyCode == keyCodeFor("h")) {
e.preventDefault();
}
}
}
else if (e.altKey) {
// Only toggle screens if not currently using fullscreen textbox
if (document.getElementById("fullScreenTextboxScreen").style.display == "none") {
if (keyCode == keyCodeFor("s")) {
e.preventDefault();
ToggleSettingsScreen(true);
}
else if (keyCode == keyCodeFor("a")) {
e.preventDefault();
ToggleAccountSettings();
}
}
}
}, false);
}
function SubmitWordOnCtrlEnter(keypress) {
var keyCode = (event.which ? event.which : event.keyCode);
if (keyCode === keyCodeFor("ctrlEnter") || (keyCode == keyCodeFor("enter") && event.ctrlKey)) { //Windows and Linux Chrome accept ctrl+enter as keyCode 10.
event.preventDefault();
AddWord();
if (document.getElementById("newWordButtonArea").style.display == "none" && document.getElementById("editWordButtonArea").style.display == "none") {
document.getElementById("updateConfirmButton").focus();
}
}
}
function LoadUserDictionaries() {
var getDictionariesRequest = new XMLHttpRequest();
var userDictionariesSelect = document.getElementById("userDictionaries");
if (userDictionariesSelect != null) {
getDictionariesRequest.open('GET', "php/ajax_dictionarymanagement.php?action=getall");
getDictionariesRequest.onreadystatechange = function() {
if (getDictionariesRequest.readyState == 4 && getDictionariesRequest.status == 200) {
ParseUserDictionariesIntoSelect(userDictionariesSelect, getDictionariesRequest.responseText);
}
}
getDictionariesRequest.send();
}
}
function ParseUserDictionariesIntoSelect(selectToPopulate, dicitonaryList) {
if (selectToPopulate.options.length > 0) {
for (var i = selectToPopulate.options.length - 1; i >= 0; i--) {
selectToPopulate.removeChild(selectToPopulate.options[i]);
}
}
var dictionaries = dicitonaryList.split("_DICTIONARYSEPARATOR_");
for (var j = 0; j < dictionaries.length - 1; j++) {
var dictionaryOption = document.createElement('option');
var dictionaryValues = dictionaries[j].split("_IDNAMESEPARATOR_");
dictionaryOption.appendChild(document.createTextNode(htmlEntitiesParse(dictionaryValues[1])));
dictionaryOption.value = dictionaryValues[0];
selectToPopulate.appendChild(dictionaryOption);
}
selectToPopulate.value = (currentDictionary.externalID > 0) ? currentDictionary.externalID : "";
}
function GetTextFile(filename, variableName, parseMarkdown) {
parseMarkdown = (typeof parseMarkdown !== 'undefined') ? parseMarkdown : false;
var readmeFileRequest = new XMLHttpRequest();
readmeFileRequest.open('GET', filename);
readmeFileRequest.onreadystatechange = function() {
if (readmeFileRequest.readyState == 4 && readmeFileRequest.status == 200) {
window[variableName] = (parseMarkdown) ? marked(readmeFileRequest.responseText) : readmeFileRequest.responseText;
}
}
readmeFileRequest.send();
}
function ValidateLogin() {
var errorMessage = document.getElementById("loginError");
var emailValue = document.getElementById("loginEmailField").value;
var passwordValue = document.getElementById("loginPasswordField").value;
if (emailValue == "") {
errorMessage.innerHTML = "Email cannot be blank!";
return false;
} else if (!(/[^\s@]+@[^\s@]+\.[^\s@]+/.test(emailValue))) {
errorMessage.innerHTML = "Your email address looks fake. Email addresses look like this: name@email.com."
return false;
} else if (passwordValue == "") {
errorMessage.innerHTML = "Password cannot be blank!";
return false;
} else {
document.getElementById("loginForm").submit();
}
}
function ValidateCreateAccount() {
var errorMessage = document.getElementById("createAccountError");
var emailValue = document.getElementById("createAccountEmailField").value;
var passwordValue = document.getElementById("createAccountPasswordField").value;
var passwordConfirmValue = document.getElementById("createAccountPasswordConfirmField").value;
var publicNameValue = document.getElementById("createAccountPublicNameField").value;
if (emailValue == "") {
errorMessage.innerHTML = "Email cannot be blank!";
return false;
} else if (!(/[^\s@]+@[^\s@]+\.[^\s@]+/.test(emailValue))) {
errorMessage.innerHTML = "Your email address looks fake. Email addresses look like this: name@email.com."
return false;
} else if (passwordValue == "") {
errorMessage.innerHTML = "Password cannot be blank!";
return false;
} else if (passwordValue != passwordConfirmValue) {
errorMessage.innerHTML = "Passwords do not match!";
return false;
} else if (publicNameValue == "") {
errorMessage.innerHTML = "Public Name cannot be blank!";
return false;
} else {
var emailCheck = new XMLHttpRequest();
emailCheck.open('GET', "php/ajax_createaccountemailcheck.php?email=" + emailValue);
emailCheck.onreadystatechange = function() {
if (emailCheck.readyState == 4 && emailCheck.status == 200) {
if (emailCheck.responseText != "ok") {
errorMessage.innerHTML = "The email address entered is already being used. Try logging in or using a different email address instead.";
return false;
} else {
document.getElementById("createAccountForm").submit();
}
}
}
emailCheck.send();
}
}
function ValidateAccountSettings() {
var errorMessage = document.getElementById("accountSettingsError");
var emailValue = document.getElementById("accountSettingsEmailField").value;
var publicNameValue = document.getElementById("accountSettingsPublicNameField").value;
if (emailValue == "") {
errorMessage.innerHTML = "Email cannot be blank!";
return false;
} else if (!(/[^\s@]+@[^\s@]+\.[^\s@]+/.test(emailValue))) {
errorMessage.innerHTML = "Your email address looks fake. Email addresses look like this: name@email.com."
return false;
} else if (publicNameValue == "") {
errorMessage.innerHTML = "Public Name cannot be blank!";
return false;
} else {
document.getElementById("createAccountForm").submit();
}
}
function ValidateForgotPassword() {
var errorMessage = document.getElementById("forgotError");
var emailValue = document.getElementById("forgotEmailField").value;
if (emailValue == "") {
errorMessage.innerHTML = "Email cannot be blank!";
return false;
} else if (!(/[^\s@]+@[^\s@]+\.[^\s@]+/.test(emailValue))) {
errorMessage.innerHTML = "Your email address looks fake. Email addresses look like this: name@email.com."
return false;
} else {
var emailCheck = new XMLHttpRequest();
emailCheck.open('GET', "php/ajax_passwordresetemailcheck.php?email=" + emailValue);
emailCheck.onreadystatechange = function() {
if (emailCheck.readyState == 4 && emailCheck.status == 200) {
if (emailCheck.responseText != "email exists") {
errorMessage.innerHTML = "The email address entered is not in use and therefore can't have its password reset. Try <span class='clickable' onclick='ShowInfo(\"loginForm\")'>creating an account</span> instead!";
return false;
} else {
document.getElementById("forgotForm").submit();
}
}
}
emailCheck.send();
}
}
function ValidateResetPassword() {
var errorMessage = document.getElementById("resetPasswordError");
var passwordValue = document.getElementById("newPasswordField").value;
var passwordConfirmValue = document.getElementById("newPasswordConfirmField").value;
if (passwordValue == "") {
errorMessage.innerHTML = "Password cannot be blank!";
return false;
} else if (passwordValue != passwordConfirmValue) {
errorMessage.innerHTML = "Passwords do not match!";
return false;
} else {
document.getElementById("resetPasswordForm").submit();
}
}
function WarnEmailChange() {
var emailChangeWarning = document.getElementById("accountSettingsEmailChangeWarning");
var emailValue = document.getElementById("accountSettingsEmailField").value;
var originalEmailValue = document.getElementById("accountSettingsPreviousEmailField").value;
if (emailValue != originalEmailValue) {
emailChangeWarning.style.display = "block";
} else {
emailChangeWarning.style.display = "none";
}
}
function LoggedInResetPassword() {
var resetPasswordRequest = new XMLHttpRequest();
resetPasswordRequest.open('GET', "php/ajax_setnewpassword.php");
resetPasswordRequest.onreadystatechange = function() {
if (resetPasswordRequest.readyState == 4 && resetPasswordRequest.status == 200) {
if (resetPasswordRequest.responseText != "done") {
console.log(resetPasswordRequest.responseText);
alert("Error resetting password.\n\nTry again later.");
return false;
} else {
window.location = "./";
}
}
}
resetPasswordRequest.send();
}
function ExplainPublicName() {
alert("This is the name we greet you with. It's also the name displayed if you ever decide to share any of your dictionaries.\n\nNote: this is not a username, and as such is not guaranteed to be unique. Use something people will recognize you as to differentiate from other people who might use the same name!");
}
function ExplainAllowEmails() {
alert("We'll make sure that you're the first to hear about any new features that get added or if any of our policies change for any reason. We'll never spam you or sell your information, but you may need to mark emails from lexicon.ga as not spam to receive them.\nNOTE: Password reset emails will be sent regardless of your choice.");
}
function wordFormIsLocked() {
return document.getElementById("formLockButton").innerHTML == "\uD83D\uDD12";
}
function ToggleWordFormLock() {
if (wordFormIsLocked()) { //If it is already locked, change it to Unlocked and get everything working as it needs to.
UnlockWordForm();
} else {
LockWordForm();
}
}
function UnlockWordForm() {
var lockButton = document.getElementById("formLockButton");
var leftColumn = document.getElementById("leftColumn");
var wordForm = document.getElementById("wordEntryForm");
var wordFormWidth = wordForm.offsetWidth - 20;
var leftColumnWidth = leftColumn.offsetWidth;
var leftColumnHeight = leftColumn.offsetHeight;
lockButton.innerHTML = "&#128275;"; // Change to the "Unlocked lock" icon.
wordForm.style.position = "fixed";
wordForm.style.top = document.getElementsByTagName("header")[0].offsetHeight.toString() + "px";
wordForm.style.width = wordFormWidth.toString() + "px";
leftColumn.style.width = leftColumnWidth.toString() + "px";
leftColumn.style.height = leftColumnHeight.toString() + "px";
}
function LockWordForm() {
var lockButton = document.getElementById("formLockButton");
var leftColumn = document.getElementById("leftColumn");
var wordForm = document.getElementById("wordEntryForm");
lockButton.innerHTML = "&#128274;"; // Change to the "locked" icon.
leftColumn.removeAttribute('style');
wordForm.removeAttribute('style');
}
function CloseUpdateConflictArea(displayId) {
displayId = (typeof displayId !== 'undefined' && displayId != null) ? displayId : false;
if (displayId != false) {
document.getElementById(displayId).style.display = "block";
}
document.getElementById("updateConflict").style.display = "none";
EnableForm();
}
function DisableForm() {
document.getElementById("word").disabled = true;
document.getElementById("pronunciation").disabled = true;
document.getElementById("partOfSpeech").disabled = true;
document.getElementById("simpleDefinition").disabled = true;
document.getElementById("longDefinition").disabled = true;
document.getElementById("editIndex").disabled = true;
}
function EnableForm() {
document.getElementById("word").disabled = false;
document.getElementById("pronunciation").disabled = false;
document.getElementById("partOfSpeech").disabled = false;
document.getElementById("simpleDefinition").disabled = false;
document.getElementById("longDefinition").disabled = false;
document.getElementById("editIndex").disabled = false;
}
function ClearForm() {
if (document.getElementById("wordEntryForm")) {
document.getElementById("word").value = "";
document.getElementById("pronunciation").value = "";
document.getElementById("partOfSpeech").value = "";
document.getElementById("simpleDefinition").value = "";
document.getElementById("longDefinition").value = "";
document.getElementById("editIndex").value = "";
document.getElementById("newWordButtonArea").style.display = "block";
document.getElementById("editWordButtonArea").style.display = "none";
document.getElementById("errorMessage").innerHTML = "";
document.getElementById("updateConflict").style.display = "none";
EnableForm();
}
}
function ToggleDescription() {
var descriptionToggle = document.getElementById("descriptionToggle");
var descriptionArea = document.getElementById("dictionaryDescription");
if (descriptionArea.style.display == "none") {
descriptionArea.style.display = "block";
descriptionToggle.innerHTML = "Hide Description";
} else {
descriptionArea.style.display = "none";
descriptionToggle.innerHTML = "Show Description";
}
}
function ToggleSearchFilter() {
var searchFilterToggle = document.getElementById("searchFilterToggle");
var searchFilterArea = document.getElementById("searchFilterArea");
if (searchFilterArea.style.display == "none") {
searchFilterArea.style.display = "block";
searchFilterToggle.innerHTML = "Hide Search/Filter Options";
} else {
searchFilterArea.style.display = "none";
searchFilterToggle.innerHTML = "Search/Filter Options";
}
}
function ShowInfo(variableName) {
document.getElementById("infoText").innerHTML = window[variableName];
if (variableName == "loginForm") {
// document.getElementById("infoText").innerHTML = loginForm;
if (currentDictionary.words.length > 0 || currentDictionary.name != "New" || currentDictionary.description != "A new dictionary.") {
document.getElementById("dictionaryWarn").innerHTML = "If your current dictionary is not already saved to your account, be sure to <span class='exportWarnText' onclick='ExportDictionary()'>export it before logging in</span> so you don't lose anything!";
}
}
HideAccountSettings();
document.getElementById("infoPage").scrollTop = 0;
document.getElementById("infoScreen").style.display = "block";
}
function HideInfo() {
document.getElementById("infoScreen").style.display = "none";
}
function ToggleAccountSettings() {
if (document.getElementById("accountSettingsScreen")) {
var accountScreen = document.getElementById("accountSettingsScreen");
if (accountScreen.style.display == "block") {
HideAccountSettings();
} else {
ShowAccountSettings();
}
}
}
function ShowAccountSettings(variableName) {
if (document.getElementById("accountSettingsScreen"))
document.getElementById("accountSettingsScreen").style.display = "block";
HideInfo();
}
function HideAccountSettings() {
if (document.getElementById("accountSettingsScreen"))
document.getElementById("accountSettingsScreen").style.display = "none";
}
function ToggleSettingsScreen(doSave) {
var settingsScreen = document.getElementById("settingsScreen");
if (settingsScreen.style.display == "block") {
if (doSave) {
SaveSettings();
}
HideSettings();
} else {
ShowSettings();
}
}
function ShowSettings() {
document.getElementById("settingsScreen").style.display = "block";
document.getElementById("dictionaryNameEdit").value = htmlEntitiesParse(currentDictionary.name);
document.getElementById("dictionaryDescriptionEdit").value = htmlEntitiesParse(currentDictionary.description);
document.getElementById("dictionaryPartsOfSpeechEdit").value = htmlEntitiesParse(currentDictionary.settings.partsOfSpeech);
document.getElementById("dictionaryAllowDuplicates").checked = currentDictionary.settings.allowDuplicates;
document.getElementById("dictionaryCaseSensitive").checked = currentDictionary.settings.caseSensitive;
document.getElementById("dictionarySortByEquivalent").checked = currentDictionary.settings.sortByEquivalent;
document.getElementById("dictionaryIsComplete").checked = currentDictionary.settings.isComplete;
if (document.getElementById("dictionaryIsPublic")) {
document.getElementById("dictionaryIsPublic").checked = currentDictionary.settings.isPublic;
TogglePublicLink();
}
document.getElementById("numberOfWordsInDictionary").innerHTML = currentDictionary.words.length.toString();
}
function HideSettings() {
document.getElementById("settingsScreen").style.display = "none";
if (currentDictionary.settings.isComplete) {
LockWordForm();
document.getElementById("wordEntryForm").style.display = "none";
} else {
document.getElementById("wordEntryForm").style.display = "block";
}
}
function HideSettingsWhenComplete() {
if (document.getElementById("settingsScreen")) {
if (currentDictionary.settings.isComplete) {
document.getElementById("hideIfComplete").style.display = "none";
} else {
document.getElementById("hideIfComplete").style.display = "block";
}
}
}
function ShowFullScreenTextbox(textboxToExpandId, labelText) {
var sourceTextboxElement = document.getElementById(textboxToExpandId);
var targetTextboxElement = document.getElementById("fullScreenTextbox");
document.getElementById("fullScreenTextboxLabel").innerHTML = labelText;
var selection = getInputSelection(sourceTextboxElement);
document.getElementById("expandedTextboxId").innerHTML = textboxToExpandId;
targetTextboxElement.value = sourceTextboxElement.value;
document.getElementById("fullScreenTextboxScreen").style.display = "block";
setSelectionRange(targetTextboxElement, selection.start, selection.end);
}
function HideFullScreenTextbox() {
var expandedTextboxId = document.getElementById("expandedTextboxId").innerHTML;
var sourceTextboxElement = document.getElementById("fullScreenTextbox");
var targetTextboxElement = document.getElementById(expandedTextboxId);
var selection = getInputSelection(sourceTextboxElement);
targetTextboxElement.value = sourceTextboxElement.value;
document.getElementById("fullScreenTextboxScreen").style.display = "none";
setSelectionRange(targetTextboxElement, selection.start, selection.end);
}
function ShowDictionaryDeleteMenu(dictionaryList) {
document.getElementById('loadAfterDeleteScreen').style.display = 'block';
//Parse response into the list that forces you to load one and reload select in settings.
ParseUserDictionariesIntoSelect(document.getElementById("loadAfterDelete"), dictionaryList);
ParseUserDictionariesIntoSelect(document.getElementById("userDictionaries"), dictionaryList);
}
function ToggleCaseSensitiveOption() {
if (document.getElementById("dictionaryAllowDuplicates").checked) {
document.getElementById("dictionaryCaseSensitive").disabled = true;
} else {
document.getElementById("dictionaryCaseSensitive").disabled = false;
}
}
function TogglePublicLink() {
if (document.getElementById("dictionaryIsPublic").checked) {
var publicLink = "http://lexicon.ga/" + currentDictionary.externalID;
document.getElementById("publicLink").innerHTML = "<strong>Public Link:</strong><br>" + publicLink;
} else {
document.getElementById("publicLink").innerHTML = "";
}
}
function SetPartsOfSpeech () {
var partsOfSpeechSelect = document.getElementById("partOfSpeech");
var wordFilterOptions = document.getElementById("filterOptions");
var wordFiltersSelected = GetSelectedFilters();
// Clear parts of speech.
if (partsOfSpeechSelect.options.length > 0) {
for (var i = partsOfSpeechSelect.options.length - 1; i >= 0; i--) {
partsOfSpeechSelect.removeChild(partsOfSpeechSelect.options[i]);
}
wordFilterOptions.innerHTML = "";
}
// Rebuild parts of speech
var newPartsOfSpeech = htmlEntitiesParse(currentDictionary.settings.partsOfSpeech).trim().split(",");
for (var j = 0; j < newPartsOfSpeech.length; j++) {
var thePartOfSpeech = newPartsOfSpeech[j].trim();
var partOfSpeechOption = document.createElement('option');
partOfSpeechOption.appendChild(document.createTextNode(thePartOfSpeech));
partOfSpeechOption.value = thePartOfSpeech;
partsOfSpeechSelect.appendChild(partOfSpeechOption);
var wordFilterLabel = document.createElement('label');
wordFilterLabel.appendChild(document.createTextNode(thePartOfSpeech + " "));
wordFilterLabel['part-of-speech'] = thePartOfSpeech;
wordFilterLabel.className = 'filterOption';
var wordFilterCheckbox = document.createElement('input');
wordFilterCheckbox.type = 'checkbox';
wordFilterCheckbox.onchange = function(){ShowDictionary()};
if (wordFiltersSelected.indexOf(thePartOfSpeech) > -1) wordFilterCheckbox.checked = true;
wordFilterLabel.appendChild(wordFilterCheckbox);
wordFilterOptions.appendChild(wordFilterLabel);
}
}
function GetSelectedFilters() {
var wordFilterOptions = document.getElementById("filterOptions");
var wordFiltersSelected = [];
for (var i = 0; i < wordFilterOptions.children.length; i++) {
var filterOption = wordFilterOptions.children[i];
if (filterOption.children[0].checked) {
wordFiltersSelected.push(filterOption['part-of-speech']);
}
}
return wordFiltersSelected;
}
function ToggleAllFilters(doCheck) {
var wordFilterOptions = document.getElementById("filterOptions");
for (var i = 0; i < wordFilterOptions.children.length; i++) {
wordFilterOptions.children[i].children[0].checked = doCheck;
}
ShowDictionary();
}
function ShowFilterWordCount(numberOfWords) {
var filters = GetSelectedFilters();
var search = htmlEntitiesParseForSearchEntry(document.getElementById("searchBox").value);
var wordCounter = document.getElementById("filterWordCount");
if (filters.length > 0 || search != "") {
wordCounter.innerHTML = "Showing " + numberOfWords.toString() + " result" + ((numberOfWords != 1) ? "s" : "");
} else {
wordCounter.innerHTML = "";
}
}
function NewWordNotification(word) {
var notificationArea = document.getElementById("notificationArea");
var notificationMessage = document.getElementById("notificationMessage");
var wordId = currentDictionary.nextWordId - 1;
notificationArea.style.display = "block";
notificationMessage.innerHTML = "New Word Added: <a href='#" + wordId.toString() + "'>" + word + "</a>";
}
function FocusAfterAddingNewWord() {
document.getElementById("word").focus();
}

41
manifest.webmanifest Normal file
View File

@ -0,0 +1,41 @@
{
"name": "Lexiconga",
"short_name": "Lexiconga",
"icons": [
{
"src": "processedImages/icon-128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "processedImages/icon-144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "processedImages/icon-152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "processedImages/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "processedImages/icon-256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "processedImages/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "/",
"display": "standalone",
"orientation": "portrait",
"background_color": "#e6cfaa",
"theme_color": "#bd7251"
}

425
offline.html Normal file
View File

@ -0,0 +1,425 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lexiconga (OFFLINE)</title>
<meta name="description" content="The quick and easy (offline) dictionary builder for constructed languages.">
<meta name="keywords" content="conlanging, dictionary, dictionaries, lexicon, conlangs, constructed languages, glossopoeia, builder, app, tool">
<meta property="og:url" content="https://lexicon.ga/offline.html">
<meta property="og:type" content="website">
<meta property="og:title" content="Lexiconga (OFFLINE)">
<meta property="og:description" content="The quick and easy (offline) dictionary builder for constructed languages.">
<meta property="og:image" content="processedImages/social.jpg">
<meta name="twitter:card" content="summary">
<meta name="twitter:image:alt" content="Lexiconga logo">
<link rel="icon" href="processedImages/favicon.png" type="image/x-icon">
<link rel="manifest" href="manifest.webmanifest">
<meta name="theme-color" content="#000000">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Lexiconga">
<link rel="apple-touch-icon" href="processedImages/icon-152.png">
<link rel="stylesheet" href="src/main.scss">
<script>window.isOffline = true;</script>
<script src="src/index.js"></script>
</head>
<body id="defaultTheme">
<header id="top">
<a href="/" title="Lexiconga"><svg id="title" alt="Lexiconga Logo" viewBox="0 0 249.78 55.087">
<g transform="translate(-107.53 -155.84)">
<g id="lexi">
<path d="m144.03 159.39-11.339 22.409h-21.62l11.339-22.409z" />
<path d="m132.69 183.97 11.339 22.409h-21.62l-11.339-22.409z" />
<path d="m160.1 186.76v7.0042h-6.1056q-1.2442 0-1.7741 0.36864-0.50688 0.36864-0.50688 1.2442 0 0.99072 0.52992 1.3133 0.52992 0.29952 2.1658 0.29952h6.2899v8.9856h-7.3498q-5.184 0-7.9258-2.4653-2.7418-2.4883-2.7418-7.1655v-12.326q0-4.5159 2.5805-7.0042 2.6035-2.4883 7.3037-2.4883h8.1332v9.0317h-5.9674q-2.4192 0-2.4192 1.5667 0 0.92161 0.576 1.2902 0.57601 0.3456 2.0506 0.3456z" />
<path d="m187.75 174.53-8.1792 15.299 8.4557 16.151h-9.8381l-3.8707-7.6263-3.2717 7.6263h-9.6768l8.64-15.898-8.1792-15.552h9.746l3.6634 7.1424 2.9491-7.1424z" />
<path d="m198.95 174.53v31.45h-9.3543v-31.45z" />
</g>
<g id="conga">
<path d="m218.5 205.38h-3.2347q-7.6994 0-10.114-3.918-1.8223-2.9613-1.8223-11.481 0-7.5172 1.8223-10.478 2.4146-3.918 9.294-3.918h4.7381q6.5604 0 8.7928 1.8679 2.5513 2.1413 2.5513 8.4739-0.77449-0.13667-1.4123-0.13667-0.68338 0-2.0501 0.13667-1.3668 0.13668-2.0957 0.13668l-1.7768-0.13668q0-2.5057-1.5946-3.3713-1.0934-0.59227-3.6902-0.59227h-1.8679q-3.3258 0-4.5559 1.8679-0.95673 1.4579-0.95673 4.647 0 1.1845 0.0456 3.098 0.0911 1.9135 0.0911 1.959 0 4.0092 1.7768 4.9203 1.0478 0.5467 5.6493 0.5467 2.1868 0 3.0069-0.22779 2.0957-0.59226 2.0957-3.6447 0.45558-0.13668 1.139-0.27335 0.72894-0.13668 1.1845-0.13668 0.68338 0 2.0501 0.2278 1.3668 0.22779 2.0957 0.22779 0.5467 0 0.86561-0.0456 0 6.606-2.7791 8.6106-2.2324 1.6401-9.2484 1.6401zm3.0069-31.208q3.8269 0.68338 7.0616 1.549 0.63782 1.0023 1.0934 2.1412 0.50114 1.139 0.50114 2.0046l-1.5946-3.0524-3.5991-1.4123q-1.4579-0.18224-2.1868-0.18224-0.68338 0-1.6401 0h-3.8725q1.0478-0.13667 2.1868-0.36447 1.6401-0.36447 2.0501-0.68338zm10.797 13.303q-0.50115 0-2.1868-0.45559-0.82006-0.22779-1.3668-0.22779-0.27335 0-0.63782 0.0911-0.31891 0.0456-0.82006 0.2278-1.7312-0.18224-2.7335-0.31891-1.0023-0.13668-1.0934-0.31891l4.5559-0.13668q0.7745 0 1.5034 0 0.72894 0 1.5946 0.13668l-0.0911-8.0183q0.22779 2.1413 0.59226 4.3281 0.36447 2.1412 0.95673 4.6925h-0.27335zm-0.59226 13.531-1.2301-0.22779 0.31891-0.59227q0.0456-0.0911 0.0456-0.91117v-1.3668q0-0.91117 0.2278-1.8679 0.22779-1.0023 0.50114-1.959l0.13668 5.1481v1.7768zm-23.28-25.604q0.13668-0.0456 0.63782-0.31891 0.54671-0.27336 0.72894-0.27336l0.13668 0.18224q0.22779 0 0.68338-0.22779 0.50114-0.27336 0.72893-0.36447 0.2278-0.0911 1.6857-0.13668 1.4579-0.0911 2.7791-0.0911l-3.7358 0.68338-3.6447 0.54671zm8.6561-0.59227-0.95673 0.36447-1.9135-0.13667q1.3212-0.41003 2.688-0.82006l2.369-0.13667q-0.5467 0.13667-1.0023 0.27335-0.41003 0.0911-1.1845 0.45558zm-5.786 14.032q0-0.5467 0.0911-1.0023 0.13668-0.50114 0.36447-1.139v0.50114q0 0.13668 0.31891 2.1413 0.0911 0.59226 0.0911 1.2756v5.5582l-0.86562-4.1458 0.18224-0.59226q0-0.0911-0.0911-1.139-0.0911-1.0478-0.0911-1.4579zm0.72894-5.4215 0.82006-0.63782q-0.36447 0.82005-0.36447 1.6401v0.31891l-0.68338 1.3668zm0.45559 13.759q0.18223 0.0455 1.0023 0.31891 1.0934 0.27335 1.6857 0.27335l3.3258-0.68338q0.27335-0.0456 1.0023-0.18224 0.7745 0 1.5946 0l1.4123-2.4602q-0.36447 1.0478-0.45559 1.4579-0.22779 0.86561-0.22779 1.4579l0.0911 0.22779 0.13667-0.0456q-4.237 0.41002-8.4739 0.77449l-1.0934-1.139zm19.18 4.9659q-0.68338 0.63782-1.6857 1.2756-0.95673 0.63782-1.7312 0.91118 0.41003-0.36447 1.2756-1.0934 0.50115-0.45559 1.139-1.1845l1.0023 0.0911zm-4.8292 3.4169-1.5034 0.0456 2.7335-1.0934q-0.31891 0.22779-0.63782 0.50115-0.31891 0.22779-0.59226 0.5467zm-24.921-21.64-0.13667-4.5103 1.5946-2.2779q1.0023-0.22779 1.6401-0.77449 0.95673-1.0478 1.959-2.1413 0.31891-0.31891 1.7768-0.45558 1.4579-0.13668 3.9636-0.2278-1.0023 0.2278-2.0501 0.50115-1.0478 0.22779-1.959 0.50114-1.7768 0.63782-3.7814 2.2324-2.4146 1.9135-2.5968 3.4625zm5.3759 22.233q-2.688-2.7335-3.9636-7.4716-1.0023-3.7814-1.0023-8.5195 0.22779 1.5946 0.68338 4.647 0.45559 3.0069 1.1845 5.0114 0.13668 0.41002 0.45559 1.2756 0.31891 0.86561 0.63782 1.3668l1.6401 2.1868zm-4.9659-16.811v-2.8702q0-0.41003 0.50115-2.7335 0.50114-2.3235 1.3212-5.6493 0.27335-0.18223 1.7768-1.1845 1.549-1.0478 1.5946-1.4123l0.95673 0.82006q-0.63782 0.0911-1.8679 0.86561-0.86561 0.54671-1.6857 1.2301-0.18223 0.95673-0.50114 1.9135-0.27335 0.95673-0.63782 1.6857 0 0.0456-0.31891 0.45559-0.13668 0.22779-0.18224 0.41003zm20.957 16.447q0.59226 0 0.86561 0.0911-2.1412 0.45558-4.6925 0.95673-2.5057 0.5467-3.6902 0.5467-1.3668 0-3.2802-0.27335-1.8679-0.27335-3.1891-0.63782l-0.72894-1.0478 5.9682 0.7745q0.27335 0.0456 0.91117-0.0456 0.68338-0.0911 0.82005-0.0911 0.0911 0 1.0478 0.0911 1.0023 0.0911 1.3668 0.0911 0.59226 0 2.9158-0.31891 1.0023-0.13668 1.6857-0.13668zm-21.003-9.8407q0.31891 0.54671 0.59226 2.5513 0.0911 0.86561 0.27336 1.0478l0.50114 0.63782-0.0456 0.18224 0.18224 0.36447q0.45558 0.63782 0.59226 1.7312 0.18223 1.0478 0.31891 2.0046l2.0501 2.2779-2.4146-1.8679q-0.72894-2.0046-1.3668-4.0092-0.77449-2.5057-1.8679-6.8794l1.1845 1.959zm26.06-1.2756q-0.68338 0-1.9135-0.0911-1.1845-0.13668-2.5513-0.27336 0.68338-0.36446 3.5991-0.50114 2.9158-0.18223 2.9613-0.18223l-0.63782 0.68337-1.4579 0.36447zm-9.1117-12.027 2.8702 1.0478 0.27335 1.0934q-2.1868-1.0023-4.4192-1.549-2.2324-0.59227-4.4647-0.59227z" />
<path d="m262.61 195.81q0 0.77449-0.0911 2.6424-0.0911 1.8223-0.0911 1.959-1.2301 1.7768-2.9613 3.7358 0.91118-1.2756 1.4579-2.1868 1.0934-1.7312 1.0934-2.4146 0-2.4146-0.0911-4.4192-0.31891-6.9249-0.31891-7.426 0-0.59226-0.13668-1.4123-0.31891-1.959-0.31891-2.4146 0-0.72894 0.18224-1.5946 0.18223-0.86561 0.36447-1.7312 0.36446 3.5536 0.63782 7.7905 0.27335 4.1914 0.27335 7.4716zm-12.939 8.9295h-4.0547q-5.9682 0-8.7472-2.688-2.7791-2.688-2.7791-8.6561v-5.1026q0-5.6037 2.9158-8.2917 2.9158-2.688 8.565-2.688h3.6447q5.7859 0 8.7928 2.5513 3.1891 2.7335 3.1891 8.4283v5.1026q0 6.1504-2.688 8.7472-2.688 2.5968-8.8384 2.5968zm4.4192-16.264q0-3.098-1.0934-4.2825-1.0934-1.1845-4.1914-1.1845h-2.4146q-3.0069 0-4.1003 1.1845-1.0934 1.1845-1.0934 4.1914v4.7836q0 3.1435 1.0478 4.4647 1.0934 1.3212 4.1458 1.3212h2.4146q3.0524 0 4.1458-1.3212 1.139-1.3668 1.139-4.5103zm-1.2301-11.8q1.4123 0.45558 3.508 1.5034 1.7312 0.91117 3.4624 1.8224 0.41003 0.59226 0.68338 1.5034 0.0911 0.27335 0.22779 0.86561l0.27336-2.8702q-3.0524-0.63782-5.8315-1.2301-2.7335-0.59226-3.098-0.7745l-1.4123-1.1845q0.54671 0.0911 1.139 0.18223 0.63782 0.0456 1.0478 0.18224zm-14.67 2.0501q-1.8679 1.2756-2.4602 1.9135-0.5467 0.63782-0.82005 1.5946 1.5034-1.549 2.8702-2.8702 1.7312-1.6401 3.0524-2.5968 1.8679-0.22779 3.8269-0.50114t3.5991-0.27335q0.50114 0 1.139 0.13667 0.63782 0.13668 1.4579 0.31891-1.139 0.36447-2.2779 0.50115-1.139 0.0911-2.7791 0.0911-0.86561 0-2.4146-0.0456-1.5034-0.0456-2.1413-0.0456-0.41002 0-1.549 0.72894-0.77449 0.5467-1.5034 1.0478zm17.221 25.832q1.0934-0.50115 2.4146-1.2301 1.5946-0.86562 2.2779-1.2756l-6.3782 4.1458-2.369-1.3212q0.59227-0.13668 2.1868-0.18224 1.6401-0.0455 1.8679-0.13667zm-3.7358 1.6401q-0.68338-0.13668-1.4579-0.27335-0.72894-0.0911-1.139-0.0911l-0.27335 0.0911q-0.22779-0.0911-0.68338-0.27335-0.45558-0.13668-0.82005-0.36447zm-18.315-24.055q1.5946-1.139 3.1891-2.5057 1.9135-1.5946 3.0069-2.8246-0.82006 0.2278-1.549 0.45559-0.72893 0.22779-0.77449 0.27335-1.959 1.6857-2.8246 3.3713-0.86562 1.6401-0.86562 4.1003 0 0.45559 0.18224 1.4579 0.18223 1.0023 0.18223 1.5034l-0.13668 9.5673q-0.13667-1.2756-0.36446-2.9613-0.45559-3.3713-0.45559-5.0114 0-3.7358 0.41003-7.426zm4.4647 21.549q3.3713 1.7312 6.9705 2.0501 0.86561 0.0911 1.6401 0.18224 0.7745 0.0911 1.1845 0.27335-1.6857-0.68338-4.2825-1.2756-6.4693-1.4123-7.3805-1.7312-0.54671-0.63782-1.139-2.0957-0.59226-1.4579-1.2756-4.0547 0.72894 1.4123 2.0501 3.5991 1.7312 2.7791 2.2324 3.0524zm14.989-13.622 0.45558 0.86561-0.0911 6.1048-0.59226-1.5034q0.27335-2.0046 0.27335-3.9636l-0.0456-1.5034zm-0.31891-4.647q0.59226 0.45558 0.82005 1.4123 0.2278 0.95674 0.2278 2.0046l-0.13668 1.2301zm-10.023 0.77449q0 0.68338 0.13667 2.0501 0.13668 1.3668 0.13668 2.0957v2.1868q0 1.3212 0.22779 2.1412 0.41003 1.0934 0.68338 2.0046 0.18223-0.0456 0.82006 0.68338 0.63782 0.68338 0.82005 1.0023l-1.1845-0.36447q-1.5946-1.7768-2.0501-3.4624-0.41003-1.7312-0.41003-4.647v-1.4579q0-1.2756 0.27335-2.3235 0.31891-1.0934 1.1845-1.8224zm9.5673-2.3235q-0.36447 0.41002-1.2301 0.72893-0.82005 0.27336-1.5034 0.27336h-3.508l-1.5034-1.0934 7.745 0.0911zm-4.7836 13.986q0.59226-0.0911 1.2756-0.2278 0.68338-0.13667 1.0934-0.13667l2.4146 0.13667-3.3258 0.72894z" />
<path d="m287.51 205.02q-0.82006 0-1.6401 0-0.68338-0.13668-1.0023-0.18224l-11.891-20.274v20.866h-7.1071v-29.431q3.0524-0.36447 5.7859-0.36447 2.4602 0 5.1481 0.36447 2.1412 5.057 4.8292 10.023 2.4146 4.4192 5.7404 9.5673 0-3.2802-0.0911-5.8771-0.31891-9.1573-0.31891-9.7951 0-2.7335 0.0455-3.1436 0.0456-0.45558 0.36447-2.0046h7.0616v29.75q-0.95673 0.13668-2.9158 0.31891-1.9135 0.18224-4.0092 0.18224zm-10.114-31.39q0.0911 0.22779 0.31891 0.63782 0.22779 0.41003 0.31891 0.7745l-0.0456-0.18224q0.2278 0.45559 0.72894 1.4579 0.45559 1.2301 0.91117 2.4146l-1.2301-0.45558q-0.0911-1.0934-0.36447-2.0957-0.22779-1.0023-0.63782-2.5513zm18.725 1.3668v1.959q0 2.6424-0.27335 4.647-0.27335 1.959-1.139 3.2347 0.31891-2.6424 0.45558-5.1026 0.18224-2.4602 0.18224-4.7381zm-0.95673 30.752 0.0456-19.772q0 3.2802 0.31891 9.8407t0.31891 9.8407l-2.2779 0.59226q-1.4123 0-3.1891-0.2278-1.7768-0.22779-3.4169-0.41002l8.2005 0.13667zm-8.4283-12.665q-2.0046-3.4624-4.0092-6.9705-3.0524-5.5126-3.0524-6.8338 2.4146 3.918 3.7358 6.1504 2.4602 4.1914 2.4602 6.3782 0 0.31891-0.0911 0.68338t-0.18223 0.72894l1.139-0.13668zm-14.533-18.406q-2.5057 0.27335-5.2848 0.7745l-1.3212-1.2301zm3.8269-0.22779 1.0023 0.5467-3.3713-0.27335zm5.1026 24.966q2.1868 3.4169 4.5103 6.3782l4.5103 0.27335-4.6014 0.72894q-0.18223-0.27335-0.7745-0.7745-0.5467-0.45558-0.77449-0.91117-1.549-2.8246-3.0524-5.6948-1.7312-3.1891-3.3258-5.7404-1.959-3.098-3.6902-5.1481l1.0934-0.22779q0.27336 2.4602 2.5057 5.9682 1.7768 2.5968 3.5991 5.1481zm-15.9 5.6493q-0.27336-1.7768-0.59227-3.8269-0.27335-2.0501-0.27335-3.4624 0-2.4602 0.63782-12.392 0.27335-4.237 0.27335-7.426 0-0.82006-0.0911-1.4123t-0.31891-2.0957q-0.13668 0.63782-0.2278 1.2301-0.0911 0.5467-0.0911 1.2301 0 2.3235 0.18223 8.2006 0.18224 5.8771 0.31891 10.342 0.18224 4.4647 0.18224 9.6129zm7.6083 0.59227q0.41003-2.6424 0.50115-4.3736 0.13667-1.7768 0.13667-4.8748-0.0456-2.3235-0.0911-4.647-0.0455-2.369-0.0455-4.6925 0.63782 3.6902 0.68337 4.1003 0.0911 0.36447 0.31891 2.2324 0.2278 0.2278 0.27336 1.3212 0.0455 1.0478 0.0455 1.9135l0.18224-0.18223q-0.18224 0.5467-0.36447 1.0934-0.45559 1.4123-1.0023 1.4123-0.41003 0-0.59226-0.31891-0.13668-0.36447-0.13668-0.77449v-0.2278h1.2756l0.0456 8.155-1.2301-0.13667zm-8.4283-28.747q0 1.2756-0.41003 12.848-0.13667 3.8725-0.13667 7.6994h0.45558q0-5.1481-0.22779-11.162-0.22779-6.0137-0.68338-10.57l1.2301-1.0934q-0.22779 1.2756-0.22779 2.2779zm0.68338 29.613 0.18223-0.95673h0.50115q1.7312 0 3.7814 0.22779 2.0957 0.22779 3.7814 0.59226zm29.294-33.076 1.6401 0.59226-0.59226 0.63782q-0.86562-0.0911-1.7768-0.18224-0.91117-0.0911-1.6857-0.0911h-2.2324l-2.7335 0.0911q1.0023-0.59226 2.0046-0.82005 1.0478-0.22779 2.2324-0.22779z" />
<path d="m314.06 205.29h-3.1891q-6.4693 0-9.4306-4.2825-2.5513-3.6902-2.5513-10.478 0-7.5627 1.8223-10.752 2.4602-4.2825 9.294-4.2825h4.6014q4.8748 0 7.9728 2.2324 3.508 2.5057 3.508 7.1982h-7.426q-0.31891-1.9135-2.1868-2.5968-1.2756-0.45558-3.6902-0.45558h-1.1845q-3.2347 0-4.3281 1.7312-0.82006 1.3212-0.82006 4.7837v3.5536q0 4.5103 1.3212 5.7404 1.3212 1.1845 5.8771 1.1845 3.5991 0 4.4192-1.0478 0.68337-0.86562 0.68337-4.5559h-5.1937v-5.6948q1.4123 0 4.2825 0.18224 2.8702 0.13667 3.918 0.13667 2.2779 0 4.3281-0.31891v7.4716q0 5.6493-3.2802 8.1094-2.8702 2.1413-8.7472 2.1413zm10.114-29.203 2.0501 2.8246q0.27336 0.86561 0.54671 3.2802 0.13667 1.3212 0.36447 3.3713 0.0456-0.18223-0.45559-0.45558-0.50114-0.27335-0.50114-0.41003 0-0.0911 0.0911-0.59226 0.13668-0.50115 0.13668-0.7745 0-0.77449-0.86561-2.5968-0.86562-1.8679-1.4579-2.2324 0.22779-0.59226 0.59226-0.95673-0.18223-0.13667-0.63782-0.27335-0.41003-0.13668-0.59226-0.22779l0.72894-0.95673zm-3.098-0.31891-3.4169-0.82006q-0.95673 0.0456-1.9135 0.0911-0.95673 0.0456-1.9135 0.0456-0.31891 0-0.63782 0-0.27336 0-0.59227-0.0456l-7.016-0.63782 12.848 0.22779 2.6424 1.139zm2.2779 0.45558v0.27335l-1.7768-0.31891 1.7768 0.0456zm3.9636 14.305-0.13667 0.7745q-0.18224 0.91117-0.31891 2.5057-0.13668 1.549-0.27336 2.7791v-0.36446q0-1.1845 0.0911-3.1436 0.13668-2.0046 0.13668-2.4146 0-0.68338-0.0911-1.2301-0.0911-0.5467-0.13668-0.86561 0.13668 0.41003 0.31891 0.95673 0.2278 0.54671 0.41003 1.0023zm-16.037-7.6538h1.6401q1.549 0.63782 3.2802 1.3212 1.7312 0.63782 3.0069 0.86561l4.7836 0.18223 2.369 0.0911-1.0934 0.63782q-1.7768-0.13667-3.8269-0.27335-2.0501-0.18223-3.098-0.50114-0.18224-0.0456-1.8224-1.2301-1.6401-1.1845-1.9135-1.2756l-3.1435-0.27333zm-8.0639-6.9249q-0.68338 0.5467-2.0501 1.959-1.0023 1.0478-2.0046 2.0501 1.139-2.0957 2.2779-4.237l3.8269-0.68338zm8.9295 17.449q0-1.0934 0.0911-3.6902 0.0911-2.6424 0.0911-2.8246l0.72894 0.18224-0.0456 0.86561q0 0.86561 0.0456 2.5968t0.0456 2.6424l3.6447 1.2756 1.4579 2.688-0.27335-2.5513q-0.0456-0.45559-1.0023-0.59227-0.91117-0.18223-2.4146-0.18223-0.54671 0-1.0023 0.0456-0.41003 0-0.72894 0-0.63782 0-0.63782-0.45559zm11.207 11.162q0.63782 0 0.91118-0.59226 0.68337-1.5946 1.139-3.1891 0.45559-1.6401 0.68338-3.3258l1.139 0.45558-0.36446-0.86561q-0.13668 0.31891-0.36447 1.0023-0.18224 0.68338-0.41003 1.4579-0.86561 2.5968-1.0478 2.8702-0.41003 0.68338-2.369 2.2779-0.18223 0.18223-0.63782 0.27335-0.41002 0.0911-0.77449 0.0911l1.139-0.63783q0.54671 0.18224 0.95673 0.18224zm-15.718-10.433q0-0.22779-0.0911-3.4169-0.0911-3.1891-0.0911-4.5103v-0.5467q0-0.91117 0.18224-1.2301 0.22779-0.31891 0.5467-0.31891t0.59226 0.41003q0.0911 0.13667 0.0911 0.31891 0 1.0934-0.7745 4.4647-1.0934 4.8292-1.139 4.9659l1.6401 3.2802-0.0911 0.0456q-0.0911-0.31891-0.0911-1.139 0-0.82006-0.0911-1.0478-0.0456-0.22779-0.50115-0.86561-0.18223-0.2278-0.18223-0.41003zm1.0934 12.073q2.0501 0 6.1049-0.36447 4.0547-0.36447 6.0593-0.36447l-1.139 0.82006q-0.63782-0.36447-1.0478-0.45559-0.36447-0.0911-1.5946-0.27335-0.50115-0.0456-2.9613 0.50114-2.4602 0.54671-3.0069 0.82006zm-10.433-11.071q-0.31891 0.0911-0.50114 0.45559l1.0478 1.1845q-0.68338-1.4123-0.86562-2.5057-0.13667-1.139-0.13667-3.0069v-2.4146q0-0.59226 0.31891-3.0069 0.13667-1.0478 0.13667-1.7312 0-0.13668-0.27335-0.50115-0.27335-0.36446-0.27335-0.59226 0-0.50114 2.8246-4.6925 2.688-3.9636 2.9613-4.1914l1.8224-0.82006q-1.2756 1.0934-4.4192 5.6493-3.1435 4.5103-3.1435 5.467l-0.0456 3.9636q0 1.3212 0.13668 3.918 0.18223 2.5968 0.41002 2.8246zm-1.6857-5.9226q0.27335 2.3235 0.68338 4.4192 0.41002 2.0501 0.63782 2.4146l-0.2278 0.59226q2.4602 2.5968 4.8292 4.6014 3.098 2.6424 5.9226 4.0547 3.4169 1.7312 6.3782 1.7312l4.1914-0.27335q-1.139-0.18223-5.9682-0.45558-4.8292-0.27336-7.6538-1.3668-0.18224-0.0911-1.9135-1.6857-1.3212-1.2301-1.8679-1.7768-1.7768-1.7768-2.2779-2.369-1.4123-1.6401-1.5946-2.7335-0.13667-1.0934-0.13667-2.0501 0-0.86561 0.13667-2.6424 0.13668-1.7768 0.13668-2.5968zm12.756 8.6561 8.2006-0.0911-0.82005 0.50114-6.4693 0.0456-0.91118-0.45558zm4.6014-10.843 10.57 0.31891q-1.0023 0-2.2324 0.0456-1.1845 0-2.6424 0h-0.95673q-1.139 0-2.5968-0.0456-1.4579-0.0456-2.1413-0.31891z" />
<path d="m345.75 204.47-1.6401-6.3782h-8.1094l-1.7312 6.3782h-5.8771l7.0616-26.515h9.1117l7.0616 26.515zm-5.7404-21.504-2.9158 10.752h5.8315zm-2.1413-6.3326q0.18224 0 1.3212 0.13667 1.139 0.0911 1.7768 0.0911 0.59226 0 1.139 0 0.59226-0.0456 1.1845-0.0456h0.82005l-0.0456 0.63782q-0.18223 0-2.9613 0.0911-2.7335 0.0911-3.9636 0.0911h-2.5513q0.45559-0.13668 0.91117-0.36447 0.68338-0.36447 0.72894-0.59226 0.27335-0.0456 0.68338-0.0456t0.95673 0zm7.0616 0.63782 1.139 0.5467q0.59226 2.8246 0.86561 4.237 0.54671 2.5513 1.0934 4.237-1.0934-2.6424-1.7312-4.4647-0.86561-2.4602-1.3668-4.5559zm5.7404 22.688-0.18224 0.41003q-0.0456-0.59226-0.0911-1.2301 0-4.2825-1.0934-7.4716-0.45559-1.3212-1.8679-4.647l0.95673-0.36447q0.59226 3.5536 1.0478 5.467 0.22779 1.0023 2.2779 8.8839l-1.0478-1.0478zm1.0478 1.7312q0.36446 0.95673 0.95673 1.8223 0.95673 1.4123 1.0934 1.5946l-1.0478-0.18223q-0.13667-0.36447-0.45558-1.0478-0.27335-0.68338-0.36447-1.0934-0.0911-0.45558-0.18223-1.0934zm-17.631-22.962 0.68338-0.36446q-1.0023 3.5536-2.1413 7.5172-1.2756 4.4192-2.2324 7.5172-0.41003 1.3212-2.369 7.5627 1.3212-5.6037 2.8702-11.162 0.45558-1.6857 3.1891-11.071zm6.2871 19.454q0.86562 0 1.959 0.18223 0.36447 0.0456 1.139 0.18223l-0.18223-0.45558q-2.9158 0-4.2825 0.50114-0.31891 0.13668-2.7791 1.1845l0.54671-1.4579zm-13.394 6.2415q0.86561-1.0934 2.369-7.0616 0.95673-3.9636 1.9135-7.9728l-3.4625 15.809zm6.1048-24.647 0.27335-0.18223q-0.7745 2.9158-1.7312 6.196-0.91117 3.2802-2.0046 6.7427 0.68337-3.2347 1.4123-6.4693 1.0023-4.0547 2.0501-6.2871zm10.387 20q0.36447-0.0456 1.0023 2.5513 0.68338 2.5968 1.0478 2.5968 0.86561 0 1.8223 0t2.0046-0.0456q1.3212 0 2.0957 0.0456 0.82006 0.0456 1.5946 0.27335-0.50114 0.31891-1.6857 0.41003-1.139 0.0911-2.0046 0.0911l-5.1026-0.13667q-0.13668 0.0456-0.18224-0.13668 0-0.18223 0-0.63782v-0.68338q0-0.63782-0.18223-1.7768-0.18224-1.1845-0.41003-2.5513zm-7.745 0.77449q0.18223 0.59226 0.18223 1.4579 0 0.5467-0.0456 1.1845-0.0455 0.63782-0.0455 0.68338l0.13667 1.4123h-1.8679q-1.139 0-3.7814-0.0911-2.6424-0.0911-2.8246-0.0911 4.237 0 7.6538-0.36447 0.13667-0.45558 0.18223-1.2301 0.0456-0.77449 0.0456-0.86561 0.18223-0.0911 0.27335-1.0023 0.0911-0.95673 0.0911-1.0934zm6.0137-11.982-1.2301-2.5057 1.0934 4.237zm0.54671 4.5559-0.54671 0.22779-0.0456-0.86561v-0.27335l0.0456-0.50115zm-2.2779-8.7017-1.9135 8.7017 3.0524-0.0456 0.0911 0.41003h-3.6447l2.4146-9.0662z" />
</g>
</g>
</svg></a> <strong>You are OFFLINE</strong>
<input id="openSearchModal" title="Open Search Panel" placeholder="🔍&#xFE0E; Search"> <span id="searchResults"></span>
<section id="searchModal" class="modal" style="display:none;">
<div class="modal-background" onclick="this.parentElement.style.display='none';"></div>
<div class="modal-content">
<a class="close-button" onclick="this.parentElement.parentElement.style.display='none';">&times;&#xFE0E;</a>
<section>
<label>Search Term
<input id="searchBox" placeholder="Search term">
</label>
<a id="searchButton" class="small button">Search</a>
<a id="clearSearchButton" class="small red button">Clear</a>
<a class="small button" onclick="var options=document.getElementById('searchOptions').style;options.display=options.display=='block'?'none':'block';">
Toggle Options
</a>
</section>
<footer id="searchOptions" style="display:none;">
<div class="split">
<div class="quarter category">
<h3>Search For</h3>
</div>
<div class="three-quarter options">
<label>Case-Sensitive
<input type="checkbox" id="searchCaseSensitive">
</label>
<label>Ignore Diacritics/Accents
<input type="checkbox" id="searchIgnoreDiacritics">
</label>
<label>Exact Words
<input type="checkbox" id="searchExactWords">
</label>
<label>Translations
<input type="checkbox" id="searchOrthography">
</label>
</div>
</div>
<div class="split">
<div class="quarter category">
<h3>Include in Search</h3>
</div>
<div class="three-quarter options">
<label>Word Name
<input type="checkbox" checked id="searchIncludeName">
</label>
<label>Definition
<input type="checkbox" checked id="searchIncludeDefinition">
</label>
<label>Details
<input type="checkbox" checked id="searchIncludeDetails">
</label>
</div>
</div>
<div class="split">
<div class="quarter category">
<h3>Include Only</h3>
</div>
<div class="three-quarter options" id="searchPartsOfSpeech"></div>
</div>
</footer>
</div>
</section>
<div id="headerMenu">
<a id="settingsButton" class="button">Settings</a>
<a id="loginCreateAccountButton" hidden style="display:none;">Log In&nbsp;/ Create Account</a>
</div>
<div style="clear:both;"></div>
</header>
<main>
<aside id="sideColumn">
<div id="mobileWordFormShow">+</div>
<form id="wordForm">
<label>Word<span class="red">*</span><br>
<input id="wordName" maxlength="200">
</label>
<label>Pronunciation<a class="label-button ipa-table-button">IPA Chart</a><br>
<input id="wordPronunciation" class="ipa-field" maxlength="200"><br>
<a class="label-help-button ipa-field-help-button">Field Help</a>
</label>
<label>Part of Speech<br>
<select id="wordPartOfSpeech" class="part-of-speech-select"></select>
</label>
<label>Definition<span class="red">*</span><br>
<input id="wordDefinition" maxlength="2500" placeholder="Equivalent words">
</label>
<label>Details<span class="red">*</span><a class="label-button maximize-button">Maximize</a><br>
<textarea id="wordDetails" placeholder="Markdown formatting allowed"></textarea>
</label>
<label>
<a id="expandAdvancedForm" class="small button expand-advanced-form">Show Advanced Fields</a>
</label>
<div id="advancedForm" class="advanced-word-form" style="display:none;">
<label>Etymology / Root Words<br>
<input id="wordEtymology" maxlength="2500" placeholder="comma,separated,root,words">
</label>
<label>Related Words<br>
<input id="wordRelated" maxlength="2500" placeholder="comma,separated,related,words">
</label>
</div>
<div id="wordErrorMessage"></div>
<a class="button" id="addWordButton">Add Word</a>
</form>
</aside>
<section id="mainColumn">
<section id="detailsSection">
<h2 id="dictionaryName">Dictionary Name</h2>
<nav>
<ul>
<li>Description</li><li>Details</li><li>Stats</li><li id="editDictionaryButton">Edit</li>
</ul>
</nav>
<article id="detailsPanel" style="display:none;">
<p>The dictionary details</p>
</article>
</section>
<section class="pagination"></section>
<section id="entries">
<article class="entry">
<header>
<h4 class="word">Loading Words</h4>
</header>
<dl>
<dt class="definition">Please Wait...</dt>
</dl>
</article>
</section>
<section class="pagination"></section>
</section>
</main>
<footer id="bottom">
<a href="https://liberapay.com/robbieantenesse" target="_blank" rel="noopener" class="small button">Support Lexiconga</a>
<a href="https://blog.lexicon.ga" target="_blank" rel="noopener" class="small button">Blog</a>
<a href="https://github.com/Alamantus/Lexiconga/issues" target="_blank" rel="noopener" class="small button">Issues</a>
<a href="https://github.com/Alamantus/Lexiconga/releases" target="_blank" rel="noopener" class="small button">Updates</a>
<span class="separator">|</span>
<a class="button" id="helpInfoButton">Help</a>
<a class="button" id="termsInfoButton">Terms</a>
<a class="button" id="privacyInfoButton">Privacy</a>
</footer>
<section id="settingsModal" class="modal" style="display:none;">
<div class="modal-background" onclick="this.parentElement.style.display='none';"></div>
<div class="modal-content">
<a class="close-button" onclick="this.parentElement.parentElement.style.display='none';">&times;&#xFE0E;</a>
<section>
<form class="split two">
<div>
<h3>General Settings</h3>
<label>Use IPA Auto-Fill
<input id="settingsUseIPA" type="checkbox" checked><br />
<small>Check this to use character combinations to input International Phonetic Alphabet characters into
Pronunciation fields.</small>
</label>
<label>Use Hotkeys
<input id="settingsUseHotkeys" type="checkbox" checked><br />
<small>Check this to enable keyboard combinations to perform different helpful actions.</small>
</label>
<label>Default Theme <small>(the theme new dictionaries will use)</small>
<select id="settingsDefaultTheme">
<option value="default">Default</option>
<option value="dark">Dark</option>
<option value="light">Light</option>
<option value="blue">Blue</option>
<option value="green">Green</option>
<option value="yellow">Yellow</option>
<option value="red">Red</option>
<option value="royal">Royal</option>
<option value="mint">Mint</option>
<option value="grape">Grape</option>
</select>
</label>
<div id="accountSettings"></div>
</div>
<div id="accountActions"></div>
</form>
</section>
<footer>
<a class="button" id="settingsSave">Save</a>
<a class="button" id="settingsSaveAndClose">Save &amp; Close</a>
<a class="red button" onclick="this.parentElement.parentElement.parentElement.style.display='none';">Close Without Saving</a>
</footer>
</div>
</section>
<section id="editModal" class="modal" style="display:none;">
<div class="modal-background" onclick="this.parentElement.style.display='none';"></div>
<div class="modal-content">
<a class="close-button" onclick="this.parentElement.parentElement.style.display='none';">&times;&#xFE0E;</a>
<nav class="tabs">
<ul>
<li class="active">Description</li><li>Details</li><li>Settings</li><li>Actions</li>
</ul>
</nav>
<section id="editDescriptionTab">
<label>Name<br>
<input id="editName" maxlength="50">
<small>Won't update if left blank.</small>
</label>
<label>Specification<br>
<input id="editSpecification" maxlength="50">
<small>Won't update if left blank.</small>
</label>
<label>Description<a class="label-button maximize-button">Maximize</a><br>
<textarea id="editDescription"></textarea>
</label>
</section>
<section id="editDetailsTab" style="display:none;">
<label>Parts of Speech <small>(Comma Separated List)</small><br>
<input id="editPartsOfSpeech" maxlength="2500" placeholder="Noun,Adjective,Verb">
</label>
<label>Alphabetical Order <small>(Space Separated List)</small><br>
<input id="editAlphabeticalOrder" placeholder="a A b B c C d D ...">
<a class="label-help-button" onclick="alert('Include every letter and case! Any letters used in your words that are not specified will be sorted in the default order below your alphabetically custom-sorted words.\n\nLexiconga can only sort by single characters and will sort by the words AS ENTERED, not using orthographic translation.')">
Field Info
</a>&nbsp;
<small>Leave blank for default (case-insensitive ASCII/Unicode sorting)</small>
</label>
<h3>Phonology</h3>
<div class="split three">
<div>
<label>Consonants<br>
<small>(Space separated list)</small><br>
<input id="editConsonants" class="ipa-field" maxlength="100" placeholder="p b m n t ..."><br>
<a class="label-help-button ipa-field-help-button">Field Help</a>
<a class="label-button ipa-table-button">IPA Chart</a>
</label>
</div>
<div>
<label>Vowels<br>
<small>(Space separated list)</small><br>
<input id="editVowels" class="ipa-field" maxlength="100" placeholder="æ u e ɪ ..."><br>
<a class="label-help-button ipa-field-help-button">Field Help</a>
<a class="label-button ipa-table-button">IPA Chart</a>
</label>
</div>
<div>
<label>Polyphthongs&nbsp;/ Blends<br>
<small>(Space separated list)</small><br>
<input id="editBlends" class="ipa-field" maxlength="100" placeholder="ai ou ue ..."><br>
<a class="label-help-button ipa-field-help-button">Field Help</a>
<a class="label-button ipa-table-button">IPA Chart</a>
</label>
</div>
</div>
<label>Notes <small>(Markdown-enabled)</small><br>
<textarea id="editPhonologyNotes"></textarea>
</label>
<h3>Phonotactics</h3>
<div class="split three">
<div>
<label>Onset<br>
<small>(Comma separated list)</small><br>
<input id="editOnset" maxlength="100" placeholder="Consonants,Vowels">
</label>
</div>
<div>
<label>Nucleus<br>
<small>(Comma separated list)</small><br>
<input id="editNucleus" maxlength="100" placeholder="Vowels,Blends">
</label>
</div>
<div>
<label>Coda<br>
<small>(Comma separated list)</small><br>
<input id="editCoda" maxlength="100" placeholder="Any">
</label>
</div>
</div>
<label>Notes <small>(Markdown-enabled)</small><br>
<textarea id="editPhonotacticsNotes"></textarea>
</label>
<h3>Orthography</h3>
<label>Translations <small>(One translation per line)</small><a class="label-button maximize-button">Maximize</a><br>
<textarea id="editTranslations" placeholder="ai=I
AA=ay
ou=ow"></textarea>
<small>Use format: <code>sequence=replacement</code></small><br>
<small>Translations occur in the order specified here, so try to avoid double translations!</small>
</label>
<label>Notes <small>(Markdown-enabled)</small><a class="label-button maximize-button">Maximize</a><br>
<textarea id="editOrthography"></textarea>
</label>
<h3>Grammar</h3>
<label>Notes <small>(Markdown-enabled)</small><a class="label-button maximize-button">Maximize</a><br>
<textarea id="editGrammar"></textarea>
</label>
</section>
<section id="editSettingsTab" style="display:none;">
<label>Prevent Duplicate Words
<input type="checkbox" id="editPreventDuplicates"><br>
<small>Checking this box will prevent the creation of words with the exact same spelling.</small>
</label>
<label>Words are Case-Sensitive
<input type="checkbox" id="editCaseSensitive"><br>
<small>Checking this box will allow the creation of words with the exact same spelling if their capitalization is different.</small>
</label>
<label>Sort by Definition
<input type="checkbox" id="editSortByDefinition"><br>
<small>Checking this box will sort the words in alphabetical order based on the Definition instead of the Word.</small>
</label>
<label>Theme
<select id="editTheme">
<option value="default">Default</option>
<option value="dark">Dark</option>
<option value="light">Light</option>
<option value="blue">Blue</option>
<option value="green">Green</option>
<option value="yellow">Yellow</option>
<option value="red">Red</option>
<option value="royal">Royal</option>
<option value="mint">Mint</option>
<option value="grape">Grape</option>
</select>
</label>
<label>Custom Styling <small>(CSS Only)</small><a class="label-button maximize-button">Maximize</a><br>
<textarea id="editCustomCSS" placeholder=".orthographic-translation {font-family: serif;}"></textarea>
</label>
</section>
<section id="editActionsTab" style="display:none;">
<h3>Import&nbsp;/ Export</h3>
<div class="split two">
<div>
<p>
<label class="button">Import JSON <input type="file" id="importDictionaryFile" accept="application/json, .dict"><br>
<small>Import a previously-exported <code>JSON</code> file.</small>
</label>
</p>
<p>
<label class="button">Import Words <input type="file" id="importWordsCSV" accept="text/csv, .csv"><br>
<small>Import a CSV file of words.</small>
</label>
<a class="small button" download="Lexiconga_import-template.csv" href="data:text/csv;charset=utf-8,%22word%22,%22pronunciation%22,%22part of speech%22,%22definition%22,%22explanation%22%0A">Download an example file with the correct formatting</a>
</p>
</div>
<div>
<p>
<a class="button" id="exportDictionaryButton">Export JSON</a><br>
<small>Export your work as a <code>JSON</code> file to re-import later.</small>
</p>
<p>
<a class="button" id="exportWordsButton">Export Words</a><br>
<small>Export a CSV file of your words.</small>
</p>
</div>
</div>
<p>
<a class="red button" id="deleteDictionaryButton">Delete Dictionary</a><br>
<small>This will permanently delete your current dictionary, and it will not be possible to return it if you have not backed it up!</small>
</p>
</section>
<footer>
<a class="button" id="editSave">Save</a>
<a class="button" id="editSaveAndClose">Save &amp; Close</a>
<a class="red button" onclick="this.parentElement.parentElement.parentElement.style.display='none';">Close Without Saving</a>
</footer>
</div>
</section>
<div id="messagingSection"></div>
</body>
</html>

39
package.json Normal file
View File

@ -0,0 +1,39 @@
{
"name": "lexiconga",
"version": "2.2.1",
"description": "The quick and easy dictionary builder for constructed languages.",
"main": "template-index.html",
"repository": "https://github.com/Alamantus/Lexiconga.git",
"author": "Robbie Antenesse <dev@alamantus.com>",
"license": "UNLICENCED",
"scripts": {
"dev": "npm run process-images && concurrently \"npm run watch-js\" \"npm run watch-php\" \"npm run copy-files\"",
"watch-js": "parcel watch template-index.html offline.html template-view.html template-passwordreset.html --no-hmr --public-url /lexiconga/",
"watch-php": "cpx \"src/php/**/{*,.*}\" dist -v -w",
"build": "npm run process-images && npm run bundle-js && npm run copy-files && npm run copy-php",
"bundle-js": "parcel build template-index.html offline.html template-view.html template-passwordreset.html --no-source-maps",
"copy-files": "cpx \"node_modules/upup/dist/*.min.js\" dist -v",
"copy-php": "cpx \"src/php/**/{*,.*}\" dist",
"process-images": "node dev/resize-images.js",
"serve-frontend-only": "parcel template-index.html",
"clear": "npm run clear-dist && npm run clear-cache",
"clear-dist": "rimraf dist/{*,.*}",
"clear-cache": "rimraf .cache/{*,.*}"
},
"devDependencies": {
"autoprefixer": "9.8.6",
"concurrently": "^6.4.0",
"cpx": "^1.5.0",
"parcel-bundler": "^1.12.5",
"parcel-plugin-goodie-bag": "^2.0.0",
"rimraf": "^3.0.2",
"sass": "^1.32.6",
"sharp": "^0.29.3"
},
"dependencies": {
"marked": "^3.0.8",
"normalize.css": "^8.0.1",
"papaparse": "^5.3.0",
"upup": "^1.1.0"
}
}

View File

@ -1,13 +0,0 @@
<?php
// require_once("../required.php");
require_once('config.php');
require_once(SITE_LOCATION . '/php/functions.php');
$email = htmlspecialchars($_GET['email']);
if (EmailExists($email)) {
echo "email exists";
} else {
echo "ok";
}
?>

View File

@ -1,238 +0,0 @@
<?php
// require_once("../required.php");
require_once('config.php');
require_once(SITE_LOCATION . '/php/functions.php');
session_start();
if ($_GET['action'] == 'getall') {
Get_Dictionaries(true);
}
elseif ($_GET['action'] == 'load') {
Load_Current_Dictionary();
}
elseif ($_GET['action'] == 'new') {
Save_Current_DictionaryAsNew();
}
elseif ($_GET['action'] == 'update') {
Update_Current_Dictionary();
}
elseif ($_GET['action'] == 'switch') {
Switch_Current_Dictionary($_POST['newdictionaryid'], true);
}
elseif ($_GET['action'] == 'delete') {
Delete_Current_Dictionary();
}
function Get_Dictionaries($return_list = true) {
if ($_SESSION['user'] > 0) {
$query = "SELECT `id`, `name` FROM `dictionaries` WHERE `user`=" . $_SESSION['user'] . " ORDER BY `name` ASC;";
$dictionaries = query($query);
if ($dictionaries) {
if (num_rows($dictionaries) > 0) {
if ($return_list) {
$list = "";
$_SESSION['dictionaries'] = [];
while ($dict = fetch($dictionaries)) {
$_SESSION['dictionaries'][] = $dict['id']; // Save a list of all dictionaries user has.
//list for the switch dictionaries dropdown.
$list .= $dict['id'] . '_IDNAMESEPARATOR_' . $dict['name'] . '_DICTIONARYSEPARATOR_';
}
echo $list;
}
return true;
} else {
echo "no dictionaries";
}
} else {
echo "could not load";
}
} else {
echo "not signed in";
}
return false;
}
function Load_Current_Dictionary() {
if ($_SESSION['user'] > 0) {
$query = "SELECT `d`.`id`, `d`.`name`, `d`.`description`, `u`.`public_name`, `d`.`words`, `d`.`next_word_id`, `d`.`allow_duplicates`, `d`.`case_sensitive`, `d`.`parts_of_speech`, `d`.`sort_by_equivalent`, `d`.`is_complete`, `d`.`is_public` ";
$query .= "FROM `dictionaries` AS `d` LEFT JOIN `users` AS `u` ON `user`=`u`.`id` WHERE `is_current`=1 AND `user`=" . $_SESSION['user'] . ";";
$dictionary = query($query);
if ($dictionary) {
if (num_rows($dictionary) > 0) {
if (num_rows($dictionary) === 1) {
while ($dict = fetch($dictionary)) {
$_SESSION['dictionary'] = $dict['id'];
$json = '{"name":"' . $dict['name'] . '",';
$json .= '"description":"' . $dict['description'] . '",';
$json .= '"createdBy":"' . $dict['public_name'] . '",';
$json .= '"words":' . $dict['words'] . ',';
$json .= '"nextWordId":' . $dict['next_word_id'] . ',';
$json .= '"settings":{';
$json .= '"allowDuplicates":' . (($dict['allow_duplicates'] == 1) ? 'true' : 'false') . ',';
$json .= '"caseSensitive":' . (($dict['case_sensitive'] == 1) ? 'true' : 'false') . ',';
$json .= '"partsOfSpeech":"' . $dict['parts_of_speech'] . '",';
$json .= '"sortByEquivalent":' . (($dict['sort_by_equivalent'] == 1) ? 'true' : 'false') . ',';
$json .= '"isComplete":' . (($dict['is_complete'] == 1) ? 'true' : 'false') . ',';
$json .= '"isPublic":' . (($dict['is_public'] == 1) ? 'true' : 'false') . '},';
$json .= '"externalID":' . $dict['id'] . '}';
echo $json;
return true;
}
} else {
echo "more than 1 returned";
}
} else {
echo "no dictionaries";
}
} else {
echo "could not load";
}
} else {
echo "not logged in";
}
return false;
}
function Save_Current_DictionaryAsNew() {
if ($_SESSION['user'] > 0) {
$dbconnection = new PDO('mysql:host=' . DATABASE_SERVERNAME . ';dbname=' . DATABASE_NAME . ';charset=utf8', DATABASE_USERNAME, DATABASE_PASSWORD);
$dbconnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$dbconnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbconnection->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$query = "INSERT INTO `dictionaries`(`user`, `is_current`, `name`, `description`, `words`, `next_word_id`, `allow_duplicates`, `case_sensitive`, `parts_of_speech`, `sort_by_equivalent`, `is_complete`, `is_public`) ";
$query .= "VALUES (" . $_SESSION['user'] . ",0,'" . $_POST['name'] . "','" . $_POST['description'] . "','" . $_POST['words'] . "'," . $_POST['nextwordid'] . "," . $_POST['allowduplicates'] . "," . $_POST['casesensitive'] . ",'" . $_POST['partsofspeech'] . "'," . $_POST['sortbyequivalent'] . "," . $_POST['iscomplete'] . "," . $_POST['ispublic'] . ")";
try {
$update = $dbconnection->prepare($query);
$update->execute();
$_SESSION['dictionary'] = $dbconnection->lastInsertId();
$_SESSION['dictionaries'][] = $_SESSION['dictionary']; //Add new id to valid dictionaries.
echo $_SESSION['dictionary'];
Switch_Current_Dictionary($_SESSION['dictionary'], false);
return true;
}
catch (PDOException $ex) {
$errorMessage = $dbconnection->errorInfo();
echo "could not update:\n" . $errorMessage[2] . "\n" . $query;
}
} else {
echo "not logged in";
}
return false;
}
function Update_Current_Dictionary() {
if ($_SESSION['user'] > 0) {
if (isset($_SESSION['dictionary'])) {
$query = "UPDATE `dictionaries` SET ";
if (isset($_POST['name'])) {
$query .= "`name`='" . $_POST['name'] . "', ";
}
if (isset($_POST['description'])) {
$query .= "`description`='" . $_POST['description'] . "', ";
}
if (isset($_POST['words'])) {
$query .= "`words`='" . $_POST['words'] . "', ";
}
if (isset($_POST['nextwordid'])) {
$query .= "`next_word_id`=" . $_POST['nextwordid'] . ", ";
}
if (isset($_POST['allowduplicates'])) {
$query .= "`allow_duplicates`=" . $_POST['allowduplicates'] . ", ";
}
if (isset($_POST['casesensitive'])) {
$query .= "`case_sensitive`=" . $_POST['casesensitive'] . ", ";
}
if (isset($_POST['partsofspeech'])) {
$query .= "`parts_of_speech`='" . $_POST['partsofspeech'] . "', ";
}
if (isset($_POST['sortbyequivalent'])) {
$query .= "`sort_by_equivalent`='" . $_POST['sortbyequivalent'] . "', ";
}
if (isset($_POST['iscomplete'])) {
$query .= "`is_complete`=" . $_POST['iscomplete'] . ", ";
}
if (isset($_POST['ispublic'])) {
$query .= "`is_public`=" . $_POST['ispublic'] . ", ";
}
$query .= "`last_updated`='" . date("Y-m-d H:i:s") . "'";
$query .= " WHERE `id`=" . $_SESSION['dictionary'] . " AND `user`=" . $_SESSION['user'] . ";";
$update = query($query);
if ($update) {
echo "updated successfully";
return true;
} else {
echo "could not update";
}
} else {
Save_Current_DictionaryAsNew();
}
} else {
echo "not logged in";
}
return false;
}
function Switch_Current_Dictionary($newdictionaryid, $returndictionary = true) {
if ($_SESSION['user'] > 0) {
if (isset($newdictionaryid)) {
if (in_array($newdictionaryid, $_SESSION['dictionaries'])) {
//Clear is_current from all user's dictionaries and then update the one they chose, only if the chosen dictionary is valid.
$query = "UPDATE `dictionaries` SET `is_current`=0 WHERE `user`=" . $_SESSION['user'] . ";";
$query .= "UPDATE `dictionaries` SET `is_current`=1 WHERE `id`=" . $newdictionaryid . " AND `user`=" . $_SESSION['user'] . ";";
$update = query($query);
if ($update) {
if ($returndictionary) {
Load_Current_Dictionary();
} else {
echo "dictionary switched";
}
// return true;
} else {
echo "could not update";
}
} else {
echo "invalid dictionary";
}
} else {
echo "no info provided";
}
} else {
echo "not logged in";
}
return false;
}
function Delete_Current_Dictionary() {
if ($_SESSION['user'] > 0) {
if (isset($_SESSION['dictionary'])) {
if (in_array($_SESSION['dictionary'], $_SESSION['dictionaries'])) {
//Clear is_current from all user's dictionaries and then update the one they chose, only if the chosen dictionary is valid.
$query = "DELETE FROM `dictionaries` WHERE `id`=" . $_SESSION['dictionary'] . " AND `user`=" . $_SESSION['user'] . ";";
$update = query($query);
if ($update) {
Get_Dictionaries(true);
} else {
echo "could not delete";
}
} else {
echo "invalid dictionary";
}
} else {
echo "no current dictionary";
}
} else {
echo "not logged in";
}
return false;
}
?>

View File

@ -1,13 +0,0 @@
<?php
// require_once("../required.php");
require_once('config.php');
require_once(SITE_LOCATION . '/php/functions.php');
$email = htmlspecialchars($_GET['email']);
if (EmailExists($email)) {
echo "email exists";
} else {
echo "bad email";
}
?>

View File

@ -1,13 +0,0 @@
<?php
define("ROOT", $_SERVER["DOCUMENT_ROOT"]);
define("SITE_NAME", "Lexiconga");
define("SITE_LOCATION", ROOT . "/.Lexiconga"); // For absolute file paths: SITE_LOCATION . "/whatever.php"
define("DATABASE_TYPE", "mysql"); //sqlite, mysql, pgsql
define("DATABASE_SERVERNAME", "host");
define("DATABASE_USERNAME", "username");
define("DATABASE_PASSWORD", "password");
define("DATABASE_NAME", "databasename");
?>

View File

@ -1,9 +0,0 @@
<?php
// Put your function files here, and they'll be added everywhere require.php is!
require_once(SITE_LOCATION . '/php/helpers.php');
require_once(SITE_LOCATION . '/php/plugins/easycrypt.php');
require_once(SITE_LOCATION . '/php/validation.php');
require_once(SITE_LOCATION . '/php/passwordreset.php');
?>

View File

@ -1,85 +0,0 @@
<?php
// Simplified PHP functions
function query ($query_string) {
$dbconnection = new PDO('mysql:host=' . DATABASE_SERVERNAME . ';dbname=' . DATABASE_NAME . ';charset=utf8', DATABASE_USERNAME, DATABASE_PASSWORD);
$dbconnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$dbconnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
$dbconnection->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
try {
$queryResults = $dbconnection->prepare($query_string);
$queryResults->execute();
return $queryResults;
}
catch (PDOException $ex) {
return false;
}
}
function num_rows ($query_results) {
try {
$rowcount = $query_results->rowcount();
return $rowcount;
}
catch (PDOException $ex) {
return false;
}
}
function fetch ($query_results) {
try {
$fetchassoc = $query_results->fetch();
return $fetchassoc;
}
catch (PDOException $ex) {
return false;
}
}
function get_include_contents($filename) {
if (is_file($filename)) {
ob_start();
include $filename;
return ob_get_clean();
}
return false;
}
function ordinal($number) {
// Retrieved from http://stackoverflow.com/a/3110033/3508346
$ends = array('th','st','nd','rd','th','th','th','th','th','th');
if ((($number % 100) >= 11) && (($number%100) <= 13))
return $number. 'th';
else
return $number. $ends[$number % 10];
}
function time_elapsed($secs){
// Retrieved from http://php.net/manual/en/function.time.php#108581
$bit = array(
// ' year' => $secs / 31556926 % 12,
// ' week' => $secs / 604800 % 52,
// ' day' => $secs / 86400 % 7,
// ' hour' => $secs / 3600 % 24,
' minute' => $secs / 60 % 60,
' second' => $secs % 60
);
foreach($bit as $k => $v){
if($v > 1 || $v < 1)$ret[] = $v . $k . 's';
if($v == 1)$ret[] = $v . $k;
}
array_splice($ret, count($ret)-1, 0, 'and');
//$ret[] = 'ago.';
return join(' ', $ret);
}
function random_string($length = 10) {
// Retrieved from http://stackoverflow.com/a/4356295/3508346
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[rand(0, $charactersLength - 1)];
}
return $randomString;
}
?>

View File

@ -1,210 +0,0 @@
<?php
// Notification messages based on status.
if (isset($_SESSION['current_status']) && $_SESSION['current_status'] != "") {
switch ($_SESSION['current_status']) {
case "couldnotcreate":
$notificationMessage = "Could not create account.<br>Please try again later.";
break;
case "emailcreateinvalid":
$notificationMessage = "The email address used to create your account didn't work.<br>Please try another.";
break;
case "createemailorpasswordblank":
$notificationMessage = "The create account form somehow got submitted without some essential information.<br>Please try filling it out again.";
break;
case "couldnotsendresetemail":
$notificationMessage = "For some reason, the reset email could not be sent.<br>Please try again later.";
break;
case "couldnotsetresetlink":
$notificationMessage = "The email address specified for password reset does not have an account.";
break;
case "emailresetinvalid":
$notificationMessage = "The email address specified for password reset didn't work.<br>Please try again.";
break;
case "resetemailblank":
$notificationMessage = "The password reset form somehow got submitted without some essential information.<br>Please try filling it out again.";
break;
case "loginfailed":
$notificationMessage = "We couldn't log you in because your email or password was incorrect.<br>";
$_SESSION['loginfailures'] += 1;
if ($_SESSION['loginfailures'] < 10) {
$notificationMessage .= "This is your <strong>" . ordinal($_SESSION['loginfailures']) . "</strong> failed attempt.<br>After 10 failures, you will not be able to log in for 1 hour.<br>Please try again.";
} else {
$_SESSION['loginlockouttime'] = time();
$notificationMessage .= "Since you failed to log in successfully 10 times, you may not try again for 1 hour.";
}
break;
case "emaildoesnotexist":
$notificationMessage = "The email address you entered doesn't have an account.<br>Would you like to <span class='clickable' onclick='ShowInfo(\"loginForm\")'>create an account</span>?";
break;
case "emailinvalid":
$notificationMessage = "The email address you entered didn't work.<br>Please try another.";
break;
case "resetlinkfailed":
$notificationMessage = "The reset link used is not valid. Please make sure you have copied it correctly.";
break;
case "resetlinkinvalid":
$notificationMessage = "The reset link used is not valid. Please make sure you have copied it correctly.";
break;
case "couldnotresetpassword":
$notificationMessage = "Your password could not be reset at this time. Please try again later.<br>If you remember your old password, you may still use it to log in.";
break;
case "passwordresetinvalid":
$notificationMessage = "Something went wrong in the password reset process. Please try again.";
break;
case "newpasswordblank":
$notificationMessage = "All the necessary information did not make it through for your password reset. Please try again.";
break;
case "couldnotupdatesettings":
$notificationMessage = "Could not update your account settings. Please try again.";
break;
case "accountsettingsinvalid":
$notificationMessage = "The email address you entered was either not valid or is already in use by another user. Please choose a different email address if you want to update your account email.";
break;
case "createdaccountsuccessfully":
$notificationMessage = "Your account was created successfully!<br>Please log in using the email address and password you used to create it and you can start accessing your dictionaries anywhere!";
break;
case "resetemailsent":
$notificationMessage = "The password reset link has been sent to the email you specified.<br>If you do not see it in your inbox, please check your junk mail box just in case!<br>Be sure to use the link before the end of today or else you will need to request a new one.";
break;
case "showresetform":
$notificationMessage = '<script>document.getElementById("notificationCloseButton").style.display = "none";</script>
<form id="resetPasswordForm" method="post" action="?resetpassword" style="text-align:left;">
<h2 style="margin-top: 3px;">Reset Your Password</h2>
<label><span>New Password</span>
<input type="password" id="newPasswordField" name="password" />
</label>
<label><span>Confirm Password</span>
<input type="password" id="newPasswordConfirmField" name="confirmpassword" />
</label>
<input type="hidden" name="account" value="' . Get_User_Email($_SESSION['reset_account']) . '" />
<div id="resetPasswordError" style="font-weight:bold;color:red;"></div>
<button type="submit" id="createAccountSubmitButton" onclick="ValidateResetPassword(); return false;">Set New Password</button>
</form>';
break;
case "passwordresetsuccessfully":
$notificationMessage = "Your password has been successfully reset. You may now log in using your new password.";
break;
case "accountsettingsupdated":
$notificationMessage = "Your settings have been updated.";
break;
}
$_SESSION['current_status'] = "";
}
if (isset($_GET['logout']) && $current_user > 0) {
session_destroy();
header('Location: ./?loggedout');
}
elseif (isset($_GET['login']) && $current_user <= 0) {
if (isset($_POST['email']) && isset($_POST['password'])) {
if (filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) {
if (EmailExists($_POST['email'])) {
if (Validate_Login($_POST['email'], $_POST['password'])) {
$_SESSION['user'] = Get_User_Id($_POST['email']);
} else {
$_SESSION['current_status'] = "loginfailed";
}
} else {
$_SESSION['current_status'] = "emaildoesnotexist";
}
} else {
$_SESSION['current_status'] = "emailinvalid";
}
} else {
$_SESSION['current_status'] = "loginemailorpasswordblank";
}
header('Location: ./');
}
elseif (isset($_GET['createaccount'])) {
if (isset($_POST['email']) && isset($_POST['password'])) {
if (filter_var($_POST['email'], FILTER_VALIDATE_EMAIL) && !EmailExists($_POST['email'])) {
if (query("INSERT INTO users (email, password, public_name, allow_email) VALUES ('" . $_POST['email'] . "','" . crypt($_POST['password'], $_POST['email']) . "','" . htmlspecialchars($_POST['publicname'], ENT_QUOTES) . "'," . (($_POST['allowemails'] != "on") ? 0 : 1) . ")")) {
$_SESSION['current_status'] = "createdaccountsuccessfully";
} else {
$_SESSION['current_status'] = "couldnotcreate";
}
} else {
$_SESSION['current_status'] = "emailcreateinvalid";
}
} else {
$_SESSION['current_status'] = "createemailorpasswordblank";
}
header('Location: ./');
}
elseif (isset($_GET['forgot'])) {
if (isset($_POST['email'])) {
if (filter_var($_POST['email'], FILTER_VALIDATE_EMAIL) && EmailExists($_POST['email'])) {
$reset_email = Set_Password_Reset($_POST['email']);
if ($reset_email === true) {
$_SESSION['current_status'] = "resetemailsent";
} elseif ($reset_email === "could not send") {
$_SESSION['current_status'] = "couldnotsendresetemail";
} else {
$_SESSION['current_status'] = "couldnotsetresetlink";
}
} else {
$_SESSION['current_status'] = "emailresetinvalid";
}
} else {
$_SESSION['current_status'] = "resetemailblank";
}
header('Location: ./');
}
elseif (isset($_GET['passwordreset'])) {
if (isset($_GET['account']) && isset($_GET['code'])) {
$reset_email = Check_Password_Reset($_GET['account'], $_GET['code']);
if ($reset_email == true) {
$_SESSION['current_status'] = "showresetform";
$_SESSION['reset_account'] = $_GET['account'];
} else {
$_SESSION['current_status'] = "resetlinkfailed";
}
} else {
$_SESSION['current_status'] = "resetlinkinvalid";
}
header('Location: ./');
}
elseif (isset($_GET['resetpassword'])) {
if (isset($_POST['account']) && isset($_POST['password'])) {
if (filter_var($_POST['account'], FILTER_VALIDATE_EMAIL) && EmailExists($_POST['account'])) {
$reset_password_success = Reset_Password($_POST['password'], $_POST['account']);
if ($reset_password_success == true) {
$_SESSION['current_status'] = "passwordresetsuccessfully";
} else {
$_SESSION['current_status'] = "couldnotresetpassword";
}
} else {
$_SESSION['current_status'] = "passwordresetinvalid";
}
} else {
$_SESSION['current_status'] = "newpasswordblank";
}
header('Location: ./');
}
elseif (isset($_GET['accountsettings'])) {
if (filter_var($_POST['email'], FILTER_VALIDATE_EMAIL) &&
($_POST['email'] == Get_User_Email($current_user) || !EmailExists($_POST['email'])))
{
$public_name = (isset($_POST['publicname']) && $_POST['publicname'] != "") ? $_POST['publicname'] : "Someone";
if (query("UPDATE `users` SET `email`='" . $_POST['email'] . "', `public_name`='" . htmlspecialchars($public_name, ENT_QUOTES) . "', `allow_email`=" . (($_POST['allowemails'] != "on") ? 0 : 1) . " WHERE `id`=" . $current_user . ";")) {
$_SESSION['current_status'] = "accountsettingsupdated";
} else {
$_SESSION['current_status'] = "couldnotupdatesettings";
}
} else {
$_SESSION['current_status'] = "accountsettingsinvalid";
}
header('Location: ./');
}
elseif (isset($_GET['loggedout']) && $current_user <= 0) {
$notificationMessage = "You have been successfully logged out.<br>You will only be able to use the dictionary saved to your browser.";
} elseif ($current_user > 0) {
if ($notificationMessage != "") {
$notificationMessage = "Welcome back, " . Get_Public_Name_By_Id($current_user) . "!<br>" . $notificationMessage;
} else {
$notificationMessage = "Welcome back, " . Get_Public_Name_By_Id($current_user) . "!";
}
}
?>

View File

@ -1,53 +0,0 @@
<?php
function Set_Password_Reset($email) {
$date = date("Y-m-d H:i:s");
$reset_code = random_string(20);
$query = "UPDATE `users` SET `password_reset_code`='" . $reset_code . "', `password_reset_date`='" . $date . "' WHERE `email`='" . $email . "';";
$reset = query($query);
if ($reset) {
$to = $email;
$subject = "Here's your Lexiconga password reset link";
$message = "Hello " . Get_Public_Name_By_Email($email) . "\r\n\r\nSomeone has requested a password reset link for your Lexiconga account. If it was you, you can reset your password by going to the link below and entering a new password for yourself:\r\n";
$message .= "http://lexicon.ga/?passwordreset&account=" . Get_User_Id($email) . "&code=" . $reset_code . "\r\n\r\n";
$message .= "If it wasn't you who requested the link, you can ignore this email since it was only sent to you, but you might want to consider changing your password when you have a chance.\r\n\r\n";
$message .= "The password link will only be valid for today until you use it.\r\n\r\n";
$message .= "Thanks!\r\nThe Lexiconga Admins";
$header = "From: Lexiconga Password Reset <donotreply@lexicon.ga>\r\n" .
"Reply-To: help@lexicon.ga\r\n" .
"X-Mailer: PHP/" . phpversion();
if (mail($to, $subject, $message, $header)) {
return true;
} else {
return "could not send";
}
} else {
return false;
}
}
function Check_Password_Reset($id, $code) {
$date = date("Y-m-d");
$daterange = "'" . $date . " 00:00:00' AND '" . $date . " 23:59:59'";
$query = "SELECT * FROM `users` WHERE `id`='" . $id . "' AND `password_reset_code`='" . $code . "' AND `password_reset_date` BETWEEN " . $daterange . ";";
$users = query($query);
if ($users && num_rows($users) === 1) {
return true;
} else {
return false;
}
}
function Reset_Password($password, $email) {
$query = "UPDATE `users` SET `password`='" . crypt($password, $email) . "', `password_reset_date`='0000-00-00 00:00:00' WHERE `email`='" . $email . "';";
$reset = query($query);
if ($reset) {
return true;
} else {
return false;
}
}
?>

View File

@ -1,43 +0,0 @@
<?php
/**
* simple method to encrypt or decrypt a plain text string
* initialization vector(IV) has to be the same when encrypting and decrypting
* PHP 5.4.9
*
* Retrieved from:
* https://naveensnayak.wordpress.com/2013/03/12/simple-php-encrypt-and-decrypt/
*
* this is a beginners template for simple encryption decryption
* before using this in production environments, please read about encryption
*
* @param string $action: can be 'encrypt' or 'decrypt'
* @param string $string: string to encrypt or decrypt
* @param string $key: encryption key
* @param string $iv: encryption iv
*
* @return string
*/
function easy_crypt($action, $string, $key = "Encryption-Key", $iv="What-even-is-an-IV?") {
$output = false;
$encrypt_method = "AES-256-CBC";
$secret_key = $key;
$secret_iv = $iv;
// hash
$key = hash('sha256', $secret_key);
// iv - encrypt method AES-256-CBC expects 16 bytes - else you will get a warning
$iv = substr(hash('sha256', $secret_iv), 0, 16);
if( $action == 'encrypt' ) {
$output = openssl_encrypt($string, $encrypt_method, $key, 0, $iv);
$output = base64_encode($output);
}
else if( $action == 'decrypt' ){
$output = openssl_decrypt(base64_decode($string), $encrypt_method, $key, 0, $iv);
}
return $output;
}
?>

View File

@ -1,104 +0,0 @@
<?php
function EmailExists($email) {
$query = "SELECT * FROM `users` WHERE `email`='" . $email . "'";
$users = query($query);
if ($users && num_rows($users) > 0) {
return true;
} else {
return false;
}
}
function Validate_Login($email, $password) {
$hashed_pw = crypt($password, $email);
$query = "SELECT * FROM `users` WHERE `email`='" . $email . "' AND `password`='" . $hashed_pw . "'";
$users = query($query);
if ($users && num_rows($users) === 1) {
return true;
} else {
return false;
}
}
function Get_User_Id($email) {
$query = "SELECT `id` FROM `users` WHERE `email`='" . $email . "'";
$users = query($query);
if ($users && num_rows($users) > 0) {
if (num_rows($users) === 1) {
$user = fetch($users);
return $user["id"];
} else {
return "More than one user id returned!";
}
} else {
return "No User";
}
}
function Get_User_Email($id) {
$query = "SELECT `email` FROM `users` WHERE `id`='" . $id . "'";
$users = query($query);
if ($users && num_rows($users) > 0) {
if (num_rows($users) === 1) {
$user = fetch($users);
return $user["email"];
} else {
return "More than one user id returned!";
}
} else {
return "No User";
}
}
function Get_Public_Name_By_Id($id) {
$query = "SELECT `public_name` FROM `users` WHERE `id`=" . $id . ";";
$users = query($query);
if ($users && num_rows($users) > 0) {
if (num_rows($users) === 1) {
$user = fetch($users);
return $user["public_name"];
} else {
return "More than one public name returned!";
}
} else {
return "No User";
}
}
function Get_Public_Name_By_Email($email) {
$query = "SELECT `public_name` FROM `users` WHERE `email`='" . $email . "';";
$users = query($query);
if ($users && num_rows($users) > 0) {
if (num_rows($users) === 1) {
$user = fetch($users);
return $user["public_name"];
} else {
return "More than one public name returned!";
}
} else {
return "No User";
}
}
function Get_Allow_Email_By_Id($id) {
$query = "SELECT `allow_email` FROM `users` WHERE `id`=" . $id . ";";
$users = query($query);
if ($users && num_rows($users) > 0) {
if (num_rows($users) === 1) {
$user = fetch($users);
return $user["allow_email"];
} else {
return "More than one user returned!";
}
} else {
return "No User";
}
}
?>

View File

@ -1,5 +0,0 @@
<?php
// Include this on every page with require_once('required.php'), and you shouldn't need to have any others.
require_once('php/config.php');
require_once(SITE_LOCATION . '/php/functions.php');
?>

68
src/constants.js Normal file
View File

@ -0,0 +1,68 @@
import { getTimestampInSeconds } from "./helpers";
export const MIGRATE_VERSION = '2.1.0';
export const DEFAULT_DICTIONARY = {
name: 'New',
specification: 'Dictionary',
description: 'A new dictionary.',
partsOfSpeech: ['Noun', 'Adjective', 'Verb', 'Adverb', 'Preposition', 'Pronoun', 'Conjunction'],
alphabeticalOrder: [],
details: {
phonology: {
consonants: [],
vowels: [],
blends: [],
notes: '',
},
phonotactics: {
onset: [],
nucleus: [],
coda: [],
notes: '',
},
orthography: {
translations: [],
notes: '',
},
grammar: {
notes: '',
},
},
words: [
/* {
name: '',
pronunciation: '',
partOfSpeech: '',
definition: '',
details: '',
etymology: [],
related: [],
principalParts: [],
wordId: 0
}, */
],
settings: {
allowDuplicates: false,
caseSensitive: false,
sortByDefinition: false,
theme: 'default',
customCSS: '',
isPublic: false,
},
lastUpdated: getTimestampInSeconds(),
createdOn: getTimestampInSeconds(),
version: MIGRATE_VERSION,
};
export const DEFAULT_SETTINGS = {
useIPAPronunciationField: true,
useHotkeys: true,
showAdvanced: false,
defaultTheme: 'default',
templates: [],
};
export const DEFAULT_PAGE_SIZE = 50;
export const LOCAL_STORAGE_KEY = 'dictionary';
export const SETTINGS_KEY = 'settings';

74
src/helpers.js Normal file
View File

@ -0,0 +1,74 @@
import removeDiacritics from "./js/StackOverflow/removeDiacritics";
export function cloneObject(object) {
return JSON.parse(JSON.stringify(object));
}
export function download(data, filename, type) {
var file = new Blob([data], { type });
if (window.navigator.msSaveOrOpenBlob) // IE10+
window.navigator.msSaveOrOpenBlob(file, filename);
else { // Others
var a = document.createElement("a"),
url = URL.createObjectURL(file);
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(function () {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 0);
}
}
export function getIndicesOf(searchStr, findIn, caseSensitive) {
// https://stackoverflow.com/a/3410557
const searchStrLen = searchStr.length;
if (searchStrLen == 0) {
return [];
}
let startIndex = 0, index, indices = [];
if (!caseSensitive) {
findIn = findIn.toLowerCase();
searchStr = searchStr.toLowerCase();
}
while ((index = findIn.indexOf(searchStr, startIndex)) > -1) {
indices.push(index);
startIndex = index + searchStrLen;
}
return indices;
}
export function getTimestampInSeconds() {
return Math.round(Date.now() / 1000);
}
export function removeTags(html) {
if (html) {
var tagBody = '(?:[^"\'>]|"[^"]*"|\'[^\']*\')*';
var tagOrComment = new RegExp(
'<(?:'
// Comment body.
+ '!--(?:(?:-*[^->])*--+|-?)'
// Special "raw text" elements whose content should be elided.
+ '|script\\b' + tagBody + '>[\\s\\S]*?</script\\s*'
+ '|style\\b' + tagBody + '>[\\s\\S]*?</style\\s*'
// Regular name
+ '|/?[a-z]'
+ tagBody
+ ')>',
'gi');
var oldHtml;
do {
oldHtml = html;
html = html.replace(tagOrComment, '');
} while (html !== oldHtml);
return html.replace(/</g, '&lt;');
}
return html;
}
export function slugify(string) {
return removeDiacritics(string).replace(/[^a-zA-Z0-9-_]/g, '-');
}

BIN
src/images/ad-example.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -9,12 +9,12 @@
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="9.0311108mm"
height="9.0311117mm"
viewBox="0 0 32 32.000003"
width="512"
height="512"
viewBox="0 0 480.00003 480.00003"
id="svg4162"
version="1.1"
inkscape:version="0.91 r13725"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="favicon.svg"
inkscape:export-filename="C:\Users\Robbie\Documents\Git Repositories\DictionaryBuilder\images\favicon.png"
inkscape:export-xdpi="90"
@ -28,9 +28,9 @@
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="2"
inkscape:cx="27.214321"
inkscape:cy="14.163793"
inkscape:zoom="1"
inkscape:cx="256"
inkscape:cy="257.06667"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
@ -38,11 +38,12 @@
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1600"
inkscape:window-height="837"
inkscape:window-x="-8"
inkscape:window-width="1920"
inkscape:window-height="1058"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
inkscape:window-maximized="1"
units="px" />
<metadata
id="metadata4167">
<rdf:RDF>
@ -59,22 +60,23 @@
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-294.94889,-329.57512)">
transform="translate(-294.94889,118.42491)">
<g
id="g4154"
transform="matrix(0.25948938,0,0,0.1819809,266.12628,316.75349)">
transform="matrix(3.8771366,0,0,2.7190508,-134.76396,-309.06057)"
style="stroke-width:0.0669281">
<path
sodipodi:nodetypes="ccccc"
inkscape:connector-curvature="0"
id="rect4136"
d="m 234.39344,70.455884 -42.42641,83.842656 -80.8927,0 42.42641,-83.842656 z"
style="opacity:1;fill:#ff5500;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
d="m 234.39344,70.455884 -42.42641,83.842656 h -80.8927 l 42.42641,-83.842656 z"
style="opacity:1;fill:#ff5500;fill-opacity:1;stroke:none;stroke-width:0.0669281;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
sodipodi:nodetypes="ccccc"
inkscape:connector-curvature="0"
id="rect4138"
d="m 191.96703,162.45587 42.42641,83.84266 -80.8927,0 -42.42641,-83.84266 z"
style="opacity:1;fill:#ff5500;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
d="m 191.96703,162.45587 42.42641,83.84266 h -80.8927 l -42.42641,-83.84266 z"
style="opacity:1;fill:#ff5500;fill-opacity:1;stroke:none;stroke-width:0.0669281;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

121
src/images/logo.svg Normal file
View File

@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1200"
height="630"
version="1.1"
viewBox="0 0 1125.0074 590.61647"
id="svg30"
sodipodi:docname="logo.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)">
<defs
id="defs34" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1248"
inkscape:window-height="611"
id="namedview32"
showgrid="false"
units="px"
inkscape:zoom="0.26648629"
inkscape:cx="937.32668"
inkscape:cy="197.75442"
inkscape:window-x="2322"
inkscape:window-y="321"
inkscape:window-maximized="0"
inkscape:current-layer="svg30" />
<metadata
id="metadata2">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="matrix(4.5967881,0,0,4.5967881,-505.87772,-547.68653)"
id="g28"
style="stroke-width:0.2175432">
<g
id="lexi"
style="stroke-width:0.2175432">
<path
d="m 144.03,159.39 -11.339,22.409 h -21.62 L 122.41,159.39 Z"
id="path4"
inkscape:connector-curvature="0"
style="fill:#ff5500;stroke-width:0.2175432" />
<path
d="m 132.69,183.97 11.339,22.409 h -21.62 L 111.07,183.97 Z"
id="path6"
inkscape:connector-curvature="0"
style="fill:#ff5500;stroke-width:0.2175432" />
<g
id="g14"
style="fill:#ff5400;stroke-width:0.2175432">
<path
d="m 160.1,186.76 v 7.0042 h -6.1056 q -1.2442,0 -1.7741,0.36864 -0.50688,0.36864 -0.50688,1.2442 0,0.99072 0.52992,1.3133 0.52992,0.29952 2.1658,0.29952 h 6.2899 v 8.9856 h -7.3498 q -5.184,0 -7.9258,-2.4653 -2.7418,-2.4883 -2.7418,-7.1655 v -12.326 q 0,-4.5159 2.5805,-7.0042 2.6035,-2.4883 7.3037,-2.4883 h 8.1332 v 9.0317 h -5.9674 q -2.4192,0 -2.4192,1.5667 0,0.92161 0.576,1.2902 0.57601,0.3456 2.0506,0.3456 z"
id="path8"
inkscape:connector-curvature="0"
style="stroke-width:0.2175432" />
<path
d="m 187.75,174.53 -8.1792,15.299 8.4557,16.151 h -9.8381 l -3.8707,-7.6263 -3.2717,7.6263 h -9.6768 l 8.64,-15.898 -8.1792,-15.552 h 9.746 l 3.6634,7.1424 2.9491,-7.1424 z"
id="path10"
inkscape:connector-curvature="0"
style="stroke-width:0.2175432" />
<path
d="m 198.95,174.53 v 31.45 h -9.3543 v -31.45 z"
id="path12"
inkscape:connector-curvature="0"
style="stroke-width:0.2175432" />
</g>
</g>
<g
id="conga"
style="fill:#d70000;stroke-width:0.2175432">
<path
d="m 218.5,205.38 h -3.2347 q -7.6994,0 -10.114,-3.918 -1.8223,-2.9613 -1.8223,-11.481 0,-7.5172 1.8223,-10.478 2.4146,-3.918 9.294,-3.918 h 4.7381 q 6.5604,0 8.7928,1.8679 2.5513,2.1413 2.5513,8.4739 -0.77449,-0.13667 -1.4123,-0.13667 -0.68338,0 -2.0501,0.13667 -1.3668,0.13668 -2.0957,0.13668 l -1.7768,-0.13668 q 0,-2.5057 -1.5946,-3.3713 -1.0934,-0.59227 -3.6902,-0.59227 h -1.8679 q -3.3258,0 -4.5559,1.8679 -0.95673,1.4579 -0.95673,4.647 0,1.1845 0.0456,3.098 0.0911,1.9135 0.0911,1.959 0,4.0092 1.7768,4.9203 1.0478,0.5467 5.6493,0.5467 2.1868,0 3.0069,-0.22779 2.0957,-0.59226 2.0957,-3.6447 0.45558,-0.13668 1.139,-0.27335 0.72894,-0.13668 1.1845,-0.13668 0.68338,0 2.0501,0.2278 1.3668,0.22779 2.0957,0.22779 0.5467,0 0.86561,-0.0456 0,6.606 -2.7791,8.6106 -2.2324,1.6401 -9.2484,1.6401 z m 3.0069,-31.208 q 3.8269,0.68338 7.0616,1.549 0.63782,1.0023 1.0934,2.1412 0.50114,1.139 0.50114,2.0046 l -1.5946,-3.0524 -3.5991,-1.4123 q -1.4579,-0.18224 -2.1868,-0.18224 -0.68338,0 -1.6401,0 h -3.8725 q 1.0478,-0.13667 2.1868,-0.36447 1.6401,-0.36447 2.0501,-0.68338 z m 10.797,13.303 q -0.50115,0 -2.1868,-0.45559 -0.82006,-0.22779 -1.3668,-0.22779 -0.27335,0 -0.63782,0.0911 -0.31891,0.0456 -0.82006,0.2278 -1.7312,-0.18224 -2.7335,-0.31891 -1.0023,-0.13668 -1.0934,-0.31891 l 4.5559,-0.13668 q 0.7745,0 1.5034,0 0.72894,0 1.5946,0.13668 l -0.0911,-8.0183 q 0.22779,2.1413 0.59226,4.3281 0.36447,2.1412 0.95673,4.6925 h -0.27335 z m -0.59226,13.531 -1.2301,-0.22779 0.31891,-0.59227 q 0.0456,-0.0911 0.0456,-0.91117 v -1.3668 q 0,-0.91117 0.2278,-1.8679 0.22779,-1.0023 0.50114,-1.959 l 0.13668,5.1481 v 1.7768 z m -23.28,-25.604 q 0.13668,-0.0456 0.63782,-0.31891 0.54671,-0.27336 0.72894,-0.27336 l 0.13668,0.18224 q 0.22779,0 0.68338,-0.22779 0.50114,-0.27336 0.72893,-0.36447 0.2278,-0.0911 1.6857,-0.13668 1.4579,-0.0911 2.7791,-0.0911 l -3.7358,0.68338 -3.6447,0.54671 z m 8.6561,-0.59227 -0.95673,0.36447 -1.9135,-0.13667 q 1.3212,-0.41003 2.688,-0.82006 l 2.369,-0.13667 q -0.5467,0.13667 -1.0023,0.27335 -0.41003,0.0911 -1.1845,0.45558 z m -5.786,14.032 q 0,-0.5467 0.0911,-1.0023 0.13668,-0.50114 0.36447,-1.139 v 0.50114 q 0,0.13668 0.31891,2.1413 0.0911,0.59226 0.0911,1.2756 v 5.5582 l -0.86562,-4.1458 0.18224,-0.59226 q 0,-0.0911 -0.0911,-1.139 -0.0911,-1.0478 -0.0911,-1.4579 z m 0.72894,-5.4215 0.82006,-0.63782 q -0.36447,0.82005 -0.36447,1.6401 v 0.31891 l -0.68338,1.3668 z m 0.45559,13.759 q 0.18223,0.0455 1.0023,0.31891 1.0934,0.27335 1.6857,0.27335 l 3.3258,-0.68338 q 0.27335,-0.0456 1.0023,-0.18224 0.7745,0 1.5946,0 l 1.4123,-2.4602 q -0.36447,1.0478 -0.45559,1.4579 -0.22779,0.86561 -0.22779,1.4579 l 0.0911,0.22779 0.13667,-0.0456 q -4.237,0.41002 -8.4739,0.77449 l -1.0934,-1.139 z m 19.18,4.9659 q -0.68338,0.63782 -1.6857,1.2756 -0.95673,0.63782 -1.7312,0.91118 0.41003,-0.36447 1.2756,-1.0934 0.50115,-0.45559 1.139,-1.1845 l 1.0023,0.0911 z m -4.8292,3.4169 -1.5034,0.0456 2.7335,-1.0934 q -0.31891,0.22779 -0.63782,0.50115 -0.31891,0.22779 -0.59226,0.5467 z m -24.921,-21.64 -0.13667,-4.5103 1.5946,-2.2779 q 1.0023,-0.22779 1.6401,-0.77449 0.95673,-1.0478 1.959,-2.1413 0.31891,-0.31891 1.7768,-0.45558 1.4579,-0.13668 3.9636,-0.2278 -1.0023,0.2278 -2.0501,0.50115 -1.0478,0.22779 -1.959,0.50114 -1.7768,0.63782 -3.7814,2.2324 -2.4146,1.9135 -2.5968,3.4625 z m 5.3759,22.233 q -2.688,-2.7335 -3.9636,-7.4716 -1.0023,-3.7814 -1.0023,-8.5195 0.22779,1.5946 0.68338,4.647 0.45559,3.0069 1.1845,5.0114 0.13668,0.41002 0.45559,1.2756 0.31891,0.86561 0.63782,1.3668 l 1.6401,2.1868 z m -4.9659,-16.811 v -2.8702 q 0,-0.41003 0.50115,-2.7335 0.50114,-2.3235 1.3212,-5.6493 0.27335,-0.18223 1.7768,-1.1845 1.549,-1.0478 1.5946,-1.4123 l 0.95673,0.82006 q -0.63782,0.0911 -1.8679,0.86561 -0.86561,0.54671 -1.6857,1.2301 -0.18223,0.95673 -0.50114,1.9135 -0.27335,0.95673 -0.63782,1.6857 0,0.0456 -0.31891,0.45559 -0.13668,0.22779 -0.18224,0.41003 z m 20.957,16.447 q 0.59226,0 0.86561,0.0911 -2.1412,0.45558 -4.6925,0.95673 -2.5057,0.5467 -3.6902,0.5467 -1.3668,0 -3.2802,-0.27335 -1.8679,-0.27335 -3.1891,-0.63782 l -0.72894,-1.0478 5.9682,0.7745 q 0.27335,0.0456 0.91117,-0.0456 0.68338,-0.0911 0.82005,-0.0911 0.0911,0 1.0478,0.0911 1.0023,0.0911 1.3668,0.0911 0.59226,0 2.9158,-0.31891 1.0023,-0.13668 1.6857,-0.13668 z m -21.003,-9.8407 q 0.31891,0.54671 0.59226,2.5513 0.0911,0.86561 0.27336,1.0478 l 0.50114,0.63782 -0.0456,0.18224 0.18224,0.36447 q 0.45558,0.63782 0.59226,1.7312 0.18223,1.0478 0.31891,2.0046 l 2.0501,2.2779 -2.4146,-1.8679 q -0.72894,-2.0046 -1.3668,-4.0092 -0.77449,-2.5057 -1.8679,-6.8794 l 1.1845,1.959 z m 26.06,-1.2756 q -0.68338,0 -1.9135,-0.0911 -1.1845,-0.13668 -2.5513,-0.27336 0.68338,-0.36446 3.5991,-0.50114 2.9158,-0.18223 2.9613,-0.18223 l -0.63782,0.68337 -1.4579,0.36447 z m -9.1117,-12.027 2.8702,1.0478 0.27335,1.0934 q -2.1868,-1.0023 -4.4192,-1.549 -2.2324,-0.59227 -4.4647,-0.59227 z"
id="path17"
inkscape:connector-curvature="0"
style="stroke-width:0.2175432" />
<path
d="m 262.61,195.81 q 0,0.77449 -0.0911,2.6424 -0.0911,1.8223 -0.0911,1.959 -1.2301,1.7768 -2.9613,3.7358 0.91118,-1.2756 1.4579,-2.1868 1.0934,-1.7312 1.0934,-2.4146 0,-2.4146 -0.0911,-4.4192 -0.31891,-6.9249 -0.31891,-7.426 0,-0.59226 -0.13668,-1.4123 -0.31891,-1.959 -0.31891,-2.4146 0,-0.72894 0.18224,-1.5946 0.18223,-0.86561 0.36447,-1.7312 0.36446,3.5536 0.63782,7.7905 0.27335,4.1914 0.27335,7.4716 z m -12.939,8.9295 h -4.0547 q -5.9682,0 -8.7472,-2.688 -2.7791,-2.688 -2.7791,-8.6561 v -5.1026 q 0,-5.6037 2.9158,-8.2917 2.9158,-2.688 8.565,-2.688 h 3.6447 q 5.7859,0 8.7928,2.5513 3.1891,2.7335 3.1891,8.4283 v 5.1026 q 0,6.1504 -2.688,8.7472 -2.688,2.5968 -8.8384,2.5968 z m 4.4192,-16.264 q 0,-3.098 -1.0934,-4.2825 -1.0934,-1.1845 -4.1914,-1.1845 h -2.4146 q -3.0069,0 -4.1003,1.1845 -1.0934,1.1845 -1.0934,4.1914 v 4.7836 q 0,3.1435 1.0478,4.4647 1.0934,1.3212 4.1458,1.3212 h 2.4146 q 3.0524,0 4.1458,-1.3212 1.139,-1.3668 1.139,-4.5103 z m -1.2301,-11.8 q 1.4123,0.45558 3.508,1.5034 1.7312,0.91117 3.4624,1.8224 0.41003,0.59226 0.68338,1.5034 0.0911,0.27335 0.22779,0.86561 l 0.27336,-2.8702 q -3.0524,-0.63782 -5.8315,-1.2301 -2.7335,-0.59226 -3.098,-0.7745 l -1.4123,-1.1845 q 0.54671,0.0911 1.139,0.18223 0.63782,0.0456 1.0478,0.18224 z m -14.67,2.0501 q -1.8679,1.2756 -2.4602,1.9135 -0.5467,0.63782 -0.82005,1.5946 1.5034,-1.549 2.8702,-2.8702 1.7312,-1.6401 3.0524,-2.5968 1.8679,-0.22779 3.8269,-0.50114 1.959,-0.27335 3.5991,-0.27335 0.50114,0 1.139,0.13667 0.63782,0.13668 1.4579,0.31891 -1.139,0.36447 -2.2779,0.50115 -1.139,0.0911 -2.7791,0.0911 -0.86561,0 -2.4146,-0.0456 -1.5034,-0.0456 -2.1413,-0.0456 -0.41002,0 -1.549,0.72894 -0.77449,0.5467 -1.5034,1.0478 z m 17.221,25.832 q 1.0934,-0.50115 2.4146,-1.2301 1.5946,-0.86562 2.2779,-1.2756 l -6.3782,4.1458 -2.369,-1.3212 q 0.59227,-0.13668 2.1868,-0.18224 1.6401,-0.0455 1.8679,-0.13667 z m -3.7358,1.6401 q -0.68338,-0.13668 -1.4579,-0.27335 -0.72894,-0.0911 -1.139,-0.0911 l -0.27335,0.0911 q -0.22779,-0.0911 -0.68338,-0.27335 -0.45558,-0.13668 -0.82005,-0.36447 z m -18.315,-24.055 q 1.5946,-1.139 3.1891,-2.5057 1.9135,-1.5946 3.0069,-2.8246 -0.82006,0.2278 -1.549,0.45559 -0.72893,0.22779 -0.77449,0.27335 -1.959,1.6857 -2.8246,3.3713 -0.86562,1.6401 -0.86562,4.1003 0,0.45559 0.18224,1.4579 0.18223,1.0023 0.18223,1.5034 l -0.13668,9.5673 q -0.13667,-1.2756 -0.36446,-2.9613 -0.45559,-3.3713 -0.45559,-5.0114 0,-3.7358 0.41003,-7.426 z m 4.4647,21.549 q 3.3713,1.7312 6.9705,2.0501 0.86561,0.0911 1.6401,0.18224 0.7745,0.0911 1.1845,0.27335 -1.6857,-0.68338 -4.2825,-1.2756 -6.4693,-1.4123 -7.3805,-1.7312 -0.54671,-0.63782 -1.139,-2.0957 -0.59226,-1.4579 -1.2756,-4.0547 0.72894,1.4123 2.0501,3.5991 1.7312,2.7791 2.2324,3.0524 z m 14.989,-13.622 0.45558,0.86561 -0.0911,6.1048 -0.59226,-1.5034 q 0.27335,-2.0046 0.27335,-3.9636 l -0.0456,-1.5034 z m -0.31891,-4.647 q 0.59226,0.45558 0.82005,1.4123 0.2278,0.95674 0.2278,2.0046 l -0.13668,1.2301 z m -10.023,0.77449 q 0,0.68338 0.13667,2.0501 0.13668,1.3668 0.13668,2.0957 v 2.1868 q 0,1.3212 0.22779,2.1412 0.41003,1.0934 0.68338,2.0046 0.18223,-0.0456 0.82006,0.68338 0.63782,0.68338 0.82005,1.0023 l -1.1845,-0.36447 q -1.5946,-1.7768 -2.0501,-3.4624 -0.41003,-1.7312 -0.41003,-4.647 v -1.4579 q 0,-1.2756 0.27335,-2.3235 0.31891,-1.0934 1.1845,-1.8224 z m 9.5673,-2.3235 q -0.36447,0.41002 -1.2301,0.72893 -0.82005,0.27336 -1.5034,0.27336 h -3.508 l -1.5034,-1.0934 7.745,0.0911 z m -4.7836,13.986 q 0.59226,-0.0911 1.2756,-0.2278 0.68338,-0.13667 1.0934,-0.13667 l 2.4146,0.13667 -3.3258,0.72894 z"
id="path19"
inkscape:connector-curvature="0"
style="stroke-width:0.2175432" />
<path
d="m 287.51,205.02 q -0.82006,0 -1.6401,0 -0.68338,-0.13668 -1.0023,-0.18224 l -11.891,-20.274 v 20.866 h -7.1071 v -29.431 q 3.0524,-0.36447 5.7859,-0.36447 2.4602,0 5.1481,0.36447 2.1412,5.057 4.8292,10.023 2.4146,4.4192 5.7404,9.5673 0,-3.2802 -0.0911,-5.8771 -0.31891,-9.1573 -0.31891,-9.7951 0,-2.7335 0.0455,-3.1436 0.0456,-0.45558 0.36447,-2.0046 h 7.0616 v 29.75 q -0.95673,0.13668 -2.9158,0.31891 -1.9135,0.18224 -4.0092,0.18224 z m -10.114,-31.39 q 0.0911,0.22779 0.31891,0.63782 0.22779,0.41003 0.31891,0.7745 l -0.0456,-0.18224 q 0.2278,0.45559 0.72894,1.4579 0.45559,1.2301 0.91117,2.4146 l -1.2301,-0.45558 q -0.0911,-1.0934 -0.36447,-2.0957 -0.22779,-1.0023 -0.63782,-2.5513 z m 18.725,1.3668 v 1.959 q 0,2.6424 -0.27335,4.647 -0.27335,1.959 -1.139,3.2347 0.31891,-2.6424 0.45558,-5.1026 0.18224,-2.4602 0.18224,-4.7381 z m -0.95673,30.752 0.0456,-19.772 q 0,3.2802 0.31891,9.8407 0.31891,6.5605 0.31891,9.8407 l -2.2779,0.59226 q -1.4123,0 -3.1891,-0.2278 -1.7768,-0.22779 -3.4169,-0.41002 l 8.2005,0.13667 z m -8.4283,-12.665 q -2.0046,-3.4624 -4.0092,-6.9705 -3.0524,-5.5126 -3.0524,-6.8338 2.4146,3.918 3.7358,6.1504 2.4602,4.1914 2.4602,6.3782 0,0.31891 -0.0911,0.68338 -0.0911,0.36447 -0.18223,0.72894 l 1.139,-0.13668 z m -14.533,-18.406 q -2.5057,0.27335 -5.2848,0.7745 l -1.3212,-1.2301 z m 3.8269,-0.22779 1.0023,0.5467 -3.3713,-0.27335 z m 5.1026,24.966 q 2.1868,3.4169 4.5103,6.3782 l 4.5103,0.27335 -4.6014,0.72894 q -0.18223,-0.27335 -0.7745,-0.7745 -0.5467,-0.45558 -0.77449,-0.91117 -1.549,-2.8246 -3.0524,-5.6948 -1.7312,-3.1891 -3.3258,-5.7404 -1.959,-3.098 -3.6902,-5.1481 l 1.0934,-0.22779 q 0.27336,2.4602 2.5057,5.9682 1.7768,2.5968 3.5991,5.1481 z m -15.9,5.6493 q -0.27336,-1.7768 -0.59227,-3.8269 -0.27335,-2.0501 -0.27335,-3.4624 0,-2.4602 0.63782,-12.392 0.27335,-4.237 0.27335,-7.426 0,-0.82006 -0.0911,-1.4123 -0.0911,-0.59224 -0.31891,-2.0957 -0.13668,0.63782 -0.2278,1.2301 -0.0911,0.5467 -0.0911,1.2301 0,2.3235 0.18223,8.2006 0.18224,5.8771 0.31891,10.342 0.18224,4.4647 0.18224,9.6129 z m 7.6083,0.59227 q 0.41003,-2.6424 0.50115,-4.3736 0.13667,-1.7768 0.13667,-4.8748 -0.0456,-2.3235 -0.0911,-4.647 -0.0455,-2.369 -0.0455,-4.6925 0.63782,3.6902 0.68337,4.1003 0.0911,0.36447 0.31891,2.2324 0.2278,0.2278 0.27336,1.3212 0.0455,1.0478 0.0455,1.9135 l 0.18224,-0.18223 q -0.18224,0.5467 -0.36447,1.0934 -0.45559,1.4123 -1.0023,1.4123 -0.41003,0 -0.59226,-0.31891 -0.13668,-0.36447 -0.13668,-0.77449 v -0.2278 h 1.2756 l 0.0456,8.155 -1.2301,-0.13667 z m -8.4283,-28.747 q 0,1.2756 -0.41003,12.848 -0.13667,3.8725 -0.13667,7.6994 h 0.45558 q 0,-5.1481 -0.22779,-11.162 -0.22779,-6.0137 -0.68338,-10.57 l 1.2301,-1.0934 q -0.22779,1.2756 -0.22779,2.2779 z m 0.68338,29.613 0.18223,-0.95673 h 0.50115 q 1.7312,0 3.7814,0.22779 2.0957,0.22779 3.7814,0.59226 z m 29.294,-33.076 1.6401,0.59226 -0.59226,0.63782 q -0.86562,-0.0911 -1.7768,-0.18224 -0.91117,-0.0911 -1.6857,-0.0911 h -2.2324 l -2.7335,0.0911 q 1.0023,-0.59226 2.0046,-0.82005 1.0478,-0.22779 2.2324,-0.22779 z"
id="path21"
inkscape:connector-curvature="0"
style="stroke-width:0.2175432" />
<path
d="m 314.06,205.29 h -3.1891 q -6.4693,0 -9.4306,-4.2825 -2.5513,-3.6902 -2.5513,-10.478 0,-7.5627 1.8223,-10.752 2.4602,-4.2825 9.294,-4.2825 h 4.6014 q 4.8748,0 7.9728,2.2324 3.508,2.5057 3.508,7.1982 h -7.426 q -0.31891,-1.9135 -2.1868,-2.5968 -1.2756,-0.45558 -3.6902,-0.45558 H 311.6 q -3.2347,0 -4.3281,1.7312 -0.82006,1.3212 -0.82006,4.7837 v 3.5536 q 0,4.5103 1.3212,5.7404 1.3212,1.1845 5.8771,1.1845 3.5991,0 4.4192,-1.0478 0.68337,-0.86562 0.68337,-4.5559 h -5.1937 v -5.6948 q 1.4123,0 4.2825,0.18224 2.8702,0.13667 3.918,0.13667 2.2779,0 4.3281,-0.31891 v 7.4716 q 0,5.6493 -3.2802,8.1094 -2.8702,2.1413 -8.7472,2.1413 z m 10.114,-29.203 2.0501,2.8246 q 0.27336,0.86561 0.54671,3.2802 0.13667,1.3212 0.36447,3.3713 0.0456,-0.18223 -0.45559,-0.45558 -0.50114,-0.27335 -0.50114,-0.41003 0,-0.0911 0.0911,-0.59226 0.13668,-0.50115 0.13668,-0.7745 0,-0.77449 -0.86561,-2.5968 -0.86562,-1.8679 -1.4579,-2.2324 0.22779,-0.59226 0.59226,-0.95673 -0.18223,-0.13667 -0.63782,-0.27335 -0.41003,-0.13668 -0.59226,-0.22779 l 0.72894,-0.95673 z m -3.098,-0.31891 -3.4169,-0.82006 q -0.95673,0.0456 -1.9135,0.0911 -0.95673,0.0456 -1.9135,0.0456 -0.31891,0 -0.63782,0 -0.27336,0 -0.59227,-0.0456 l -7.016,-0.63782 12.848,0.22779 2.6424,1.139 z m 2.2779,0.45558 v 0.27335 l -1.7768,-0.31891 1.7768,0.0456 z m 3.9636,14.305 -0.13667,0.7745 q -0.18224,0.91117 -0.31891,2.5057 -0.13668,1.549 -0.27336,2.7791 v -0.36446 q 0,-1.1845 0.0911,-3.1436 0.13668,-2.0046 0.13668,-2.4146 0,-0.68338 -0.0911,-1.2301 -0.0911,-0.5467 -0.13668,-0.86561 0.13668,0.41003 0.31891,0.95673 0.2278,0.54671 0.41003,1.0023 z m -16.037,-7.6538 h 1.6401 q 1.549,0.63782 3.2802,1.3212 1.7312,0.63782 3.0069,0.86561 l 4.7836,0.18223 2.369,0.0911 -1.0934,0.63782 q -1.7768,-0.13667 -3.8269,-0.27335 -2.0501,-0.18223 -3.098,-0.50114 -0.18224,-0.0456 -1.8224,-1.2301 -1.6401,-1.1845 -1.9135,-1.2756 l -3.1435,-0.27333 z m -8.0639,-6.9249 q -0.68338,0.5467 -2.0501,1.959 -1.0023,1.0478 -2.0046,2.0501 1.139,-2.0957 2.2779,-4.237 l 3.8269,-0.68338 z m 8.9295,17.449 q 0,-1.0934 0.0911,-3.6902 0.0911,-2.6424 0.0911,-2.8246 l 0.72894,0.18224 -0.0456,0.86561 q 0,0.86561 0.0456,2.5968 0.0456,1.73119 0.0456,2.6424 l 3.6447,1.2756 1.4579,2.688 -0.27335,-2.5513 q -0.0456,-0.45559 -1.0023,-0.59227 -0.91117,-0.18223 -2.4146,-0.18223 -0.54671,0 -1.0023,0.0456 -0.41003,0 -0.72894,0 -0.63782,0 -0.63782,-0.45559 z m 11.207,11.162 q 0.63782,0 0.91118,-0.59226 0.68337,-1.5946 1.139,-3.1891 0.45559,-1.6401 0.68338,-3.3258 l 1.139,0.45558 -0.36446,-0.86561 q -0.13668,0.31891 -0.36447,1.0023 -0.18224,0.68338 -0.41003,1.4579 -0.86561,2.5968 -1.0478,2.8702 -0.41003,0.68338 -2.369,2.2779 -0.18223,0.18223 -0.63782,0.27335 -0.41002,0.0911 -0.77449,0.0911 l 1.139,-0.63783 q 0.54671,0.18224 0.95673,0.18224 z m -15.718,-10.433 q 0,-0.22779 -0.0911,-3.4169 -0.0911,-3.1891 -0.0911,-4.5103 v -0.5467 q 0,-0.91117 0.18224,-1.2301 0.22779,-0.31891 0.5467,-0.31891 0.31891,0 0.59226,0.41003 0.0911,0.13667 0.0911,0.31891 0,1.0934 -0.7745,4.4647 -1.0934,4.8292 -1.139,4.9659 l 1.6401,3.2802 -0.0911,0.0456 q -0.0911,-0.31891 -0.0911,-1.139 0,-0.82006 -0.0911,-1.0478 -0.0456,-0.22779 -0.50115,-0.86561 -0.18223,-0.2278 -0.18223,-0.41003 z m 1.0934,12.073 q 2.0501,0 6.1049,-0.36447 4.0547,-0.36447 6.0593,-0.36447 l -1.139,0.82006 q -0.63782,-0.36447 -1.0478,-0.45559 -0.36447,-0.0911 -1.5946,-0.27335 -0.50115,-0.0456 -2.9613,0.50114 -2.4602,0.54671 -3.0069,0.82006 z m -10.433,-11.071 q -0.31891,0.0911 -0.50114,0.45559 l 1.0478,1.1845 q -0.68338,-1.4123 -0.86562,-2.5057 -0.13667,-1.139 -0.13667,-3.0069 v -2.4146 q 0,-0.59226 0.31891,-3.0069 0.13667,-1.0478 0.13667,-1.7312 0,-0.13668 -0.27335,-0.50115 -0.27335,-0.36446 -0.27335,-0.59226 0,-0.50114 2.8246,-4.6925 2.688,-3.9636 2.9613,-4.1914 l 1.8224,-0.82006 q -1.2756,1.0934 -4.4192,5.6493 -3.1435,4.5103 -3.1435,5.467 l -0.0456,3.9636 q 0,1.3212 0.13668,3.918 0.18223,2.5968 0.41002,2.8246 z m -1.6857,-5.9226 q 0.27335,2.3235 0.68338,4.4192 0.41002,2.0501 0.63782,2.4146 l -0.2278,0.59226 q 2.4602,2.5968 4.8292,4.6014 3.098,2.6424 5.9226,4.0547 3.4169,1.7312 6.3782,1.7312 l 4.1914,-0.27335 q -1.139,-0.18223 -5.9682,-0.45558 -4.8292,-0.27336 -7.6538,-1.3668 -0.18224,-0.0911 -1.9135,-1.6857 -1.3212,-1.2301 -1.8679,-1.7768 -1.7768,-1.7768 -2.2779,-2.369 -1.4123,-1.6401 -1.5946,-2.7335 -0.13667,-1.0934 -0.13667,-2.0501 0,-0.86561 0.13667,-2.6424 0.13668,-1.7768 0.13668,-2.5968 z m 12.756,8.6561 8.2006,-0.0911 -0.82005,0.50114 -6.4693,0.0456 -0.91118,-0.45558 z m 4.6014,-10.843 10.57,0.31891 q -1.0023,0 -2.2324,0.0456 -1.1845,0 -2.6424,0 h -0.95673 q -1.139,0 -2.5968,-0.0456 -1.4579,-0.0456 -2.1413,-0.31891 z"
id="path23"
inkscape:connector-curvature="0"
style="stroke-width:0.2175432" />
<path
d="m 345.75,204.47 -1.6401,-6.3782 h -8.1094 l -1.7312,6.3782 h -5.8771 l 7.0616,-26.515 h 9.1117 l 7.0616,26.515 z m -5.7404,-21.504 -2.9158,10.752 h 5.8315 z m -2.1413,-6.3326 q 0.18224,0 1.3212,0.13667 1.139,0.0911 1.7768,0.0911 0.59226,0 1.139,0 0.59226,-0.0456 1.1845,-0.0456 h 0.82005 l -0.0456,0.63782 q -0.18223,0 -2.9613,0.0911 -2.7335,0.0911 -3.9636,0.0911 h -2.5513 q 0.45559,-0.13668 0.91117,-0.36447 0.68338,-0.36447 0.72894,-0.59226 0.27335,-0.0456 0.68338,-0.0456 0.41003,0 0.95673,0 z m 7.0616,0.63782 1.139,0.5467 q 0.59226,2.8246 0.86561,4.237 0.54671,2.5513 1.0934,4.237 -1.0934,-2.6424 -1.7312,-4.4647 -0.86561,-2.4602 -1.3668,-4.5559 z m 5.7404,22.688 -0.18224,0.41003 q -0.0456,-0.59226 -0.0911,-1.2301 0,-4.2825 -1.0934,-7.4716 -0.45559,-1.3212 -1.8679,-4.647 l 0.95673,-0.36447 q 0.59226,3.5536 1.0478,5.467 0.22779,1.0023 2.2779,8.8839 l -1.0478,-1.0478 z m 1.0478,1.7312 q 0.36446,0.95673 0.95673,1.8223 0.95673,1.4123 1.0934,1.5946 l -1.0478,-0.18223 q -0.13667,-0.36447 -0.45558,-1.0478 -0.27335,-0.68338 -0.36447,-1.0934 -0.0911,-0.45558 -0.18223,-1.0934 z m -17.631,-22.962 0.68338,-0.36446 q -1.0023,3.5536 -2.1413,7.5172 -1.2756,4.4192 -2.2324,7.5172 -0.41003,1.3212 -2.369,7.5627 1.3212,-5.6037 2.8702,-11.162 0.45558,-1.6857 3.1891,-11.071 z m 6.2871,19.454 q 0.86562,0 1.959,0.18223 0.36447,0.0456 1.139,0.18223 l -0.18223,-0.45558 q -2.9158,0 -4.2825,0.50114 -0.31891,0.13668 -2.7791,1.1845 l 0.54671,-1.4579 z m -13.394,6.2415 q 0.86561,-1.0934 2.369,-7.0616 0.95673,-3.9636 1.9135,-7.9728 l -3.4625,15.809 z m 6.1048,-24.647 0.27335,-0.18223 q -0.7745,2.9158 -1.7312,6.196 -0.91117,3.2802 -2.0046,6.7427 0.68337,-3.2347 1.4123,-6.4693 1.0023,-4.0547 2.0501,-6.2871 z m 10.387,20 q 0.36447,-0.0456 1.0023,2.5513 0.68338,2.5968 1.0478,2.5968 0.86561,0 1.8223,0 0.95669,0 2.0046,-0.0456 1.3212,0 2.0957,0.0456 0.82006,0.0456 1.5946,0.27335 -0.50114,0.31891 -1.6857,0.41003 -1.139,0.0911 -2.0046,0.0911 l -5.1026,-0.13667 q -0.13668,0.0456 -0.18224,-0.13668 0,-0.18223 0,-0.63782 v -0.68338 q 0,-0.63782 -0.18223,-1.7768 -0.18224,-1.1845 -0.41003,-2.5513 z m -7.745,0.77449 q 0.18223,0.59226 0.18223,1.4579 0,0.5467 -0.0456,1.1845 -0.0455,0.63782 -0.0455,0.68338 l 0.13667,1.4123 h -1.8679 q -1.139,0 -3.7814,-0.0911 -2.6424,-0.0911 -2.8246,-0.0911 4.237,0 7.6538,-0.36447 0.13667,-0.45558 0.18223,-1.2301 0.0456,-0.77449 0.0456,-0.86561 0.18223,-0.0911 0.27335,-1.0023 0.0911,-0.95673 0.0911,-1.0934 z m 6.0137,-11.982 -1.2301,-2.5057 1.0934,4.237 z m 0.54671,4.5559 -0.54671,0.22779 -0.0456,-0.86561 v -0.27335 l 0.0456,-0.50115 z m -2.2779,-8.7017 -1.9135,8.7017 3.0524,-0.0456 0.0911,0.41003 h -3.6447 l 2.4146,-9.0662 z"
id="path25"
inkscape:connector-curvature="0"
style="stroke-width:0.2175432" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

BIN
src/images/social.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

31
src/index.js Normal file
View File

@ -0,0 +1,31 @@
import migrate from './js/migration';
import setupListeners from './js/setupListeners';
import { renderAll } from './js/render';
import { hasToken, addMessage } from './js/utilities';
import { loadDictionary } from './js/dictionaryManagement';
import { loadSettings } from './js/settings';
function initialize() {
if (window.isOffline) {
addMessage('<strong>You are using the Offline version of Lexiconga.</strong><br>Refresh the page while connected to the internet to enable using accounts.', 0);
}
migrate();
loadSettings();
loadDictionary();
setupListeners();
if (hasToken()) {
import('./js/account/index.js').then(account => {
account.loginWithToken();
});
}
renderAll();
}
window.onload = (function (oldLoad) {
oldLoad && oldLoad();
initialize();
})(window.onload);

View File

@ -0,0 +1,373 @@
{
"!*": "ǃ",
"!/": "¡",
"\"^": "̋",
"\"_": "̏",
"#_": "̻",
"''": "ˈ",
"')": "ʼ",
"'-": "˔",
"'^": "́",
"'_": "̀",
"((": "͡",
"(_": "̜",
"))": "͜",
")_": "̹",
"+_": "̟",
"+|": "ꜛ",
",,": "ˌ",
",-": "˕",
"-'": "˔",
"-,": "˕",
"-?": "ʡ",
"-E": "ɘ",
"-H": "ħ",
"-I": "ɨ",
"-J": "ɟ",
"-L": "ɬ",
"-O": "ɵ",
"-U": "ʉ",
"-^": "̄",
"-e": "ɘ",
"-h": "ħ",
"-i": "ɨ",
"-j": "ɟ",
"-l": "ɬ",
"-o": "ɵ",
"-u": "ʉ",
"-|": "ꜜ",
"-ʔ": "ʡ",
"-ʕ": "ʢ",
"-ʡ": "ʔ",
"-ʢ": "ʕ",
"-ː": "ˑ",
"-ˑ": "ː",
"/>": "↗",
"/\\": "ʌ",
"/^": "↗",
"1|": "˩",
"2|": "˨",
"3|": "˧",
"4|": "˦",
"5|": "˥",
"::": "ː",
":^": "̈",
":_": "̤",
"<|": "⊣",
"=*": "ǁ",
">/": "↗",
">R": "˞",
">\\": "↘",
">^": "̚",
">r": "˞",
">|": "⊢",
"?-": "ʡ",
"?/": "ʕ",
"??": "ʔ",
"A/": "ɐ",
"AA": "ɑ",
"AE": "æ",
"A|": "ɑ",
"B(": "ɓ",
"BB": "ʙ",
"BH": "β",
"C,": "ç",
"C/": "ɔ",
"CE": "ɶ",
"D(": "ɗ",
"D)": "ɖ",
"DH": "ð",
"E-": "ɘ",
"E/": "ə",
"EB": "ɞ",
"EE": "ɛ",
"G(": "ɠ",
"GG": "ɢ",
"H,": "ɦ",
"H-": "ħ",
"H/": "ɥ",
"HH": "ʜ",
"H^": "ʰ",
"I-": "ɨ",
"II": "ɪ",
"J(": "ʄ",
"J,": "ʝ",
"J-": "ɟ",
"J^": "ʲ",
"K/": "ʞ",
"L)": "ɭ",
"L-": "ɬ",
"LL": "ʟ",
"LR": "ɺ",
"LZ": "ɮ",
"L^": "ˡ",
"Lɾ": "ɺ",
"M,": "ɱ",
"M/": "ɯ",
"M^": "ᵐ",
"N)": "ɳ",
"N,": "ŋ",
"NN": "ɴ",
"N^": "ⁿ",
"O*": "ʘ",
"O-": "ɵ",
"O.": "ʘ",
"O/": "ø",
"OE": "œ",
"OO": "ɞ",
"OX": "ɤ",
"O^": "̊",
"O_": "̥",
"O|": "ɑ",
"PH": "ɸ",
"R)": "ɽ",
"R/": "ɹ",
"R0": "ɾ",
"R>": "˞",
"RL": "ɺ",
"RO": "ɾ",
"RR": "ʀ",
"S)": "ʂ",
"SH": "ʃ",
"SJ": "ɕ",
"T)": "ʈ",
"TH": "θ",
"U-": "ʉ",
"UU": "ʊ",
"U^": "̆",
"U_": "̮",
"V,": "ⱱ",
"V/": "ʌ",
"V0": "ʋ",
"VO": "ʋ",
"V^": "̌",
"V_": "̬",
"W,": "ɰ",
"W/": "ʍ",
"W^": "ʷ",
"W|": "ɰ",
"X,": "ɣ",
"XO": "ɤ",
"XX": "χ",
"X^": "̽",
"Xʃ": "ɧ",
"Y/": "ʎ",
"YY": "ʏ",
"Z)": "ʐ",
"ZH": "ʒ",
"ZJ": "ʑ",
"[]": "̻",
"[_": "̪",
"\\>": "↘",
"\\V": "↘",
"\\v": "↘",
"]_": "̺",
"^\"": "̋",
"^'": "́",
"^-": "̄",
"^/": "↗",
"^:": "̈",
"^>": "̚",
"^H": "ʰ",
"^J": "ʲ",
"^L": "ˡ",
"^M": "ᵐ",
"^N": "ⁿ",
"^O": "̊",
"^U": "̆",
"^V": "̌",
"^W": "ʷ",
"^X": "̽",
"^^": "̂",
"^_": "̯",
"^h": "ʰ",
"^j": "ʲ",
"^l": "ˡ",
"^m": "ᵐ",
"^n": "ⁿ",
"^o": "̊",
"^u": "̆",
"^v": "̌",
"^w": "ʷ",
"^x": "̽",
"^|": "̍",
"^~": "̃",
"^ŋ": "ᵑ",
"^ɣ": "ˠ",
"^ɥ": "ᶣ",
"^ʋ": "ᶹ",
"^ʕ": "ˤ",
"_\"": "̏",
"_#": "̻",
"_'": "̀",
"_(": "̜",
"_)": "̹",
"_+": "̟",
"_:": "̤",
"_O": "̥",
"_U": "̮",
"_V": "̬",
"_[": "̪",
"_]": "̺",
"_^": "̯",
"__": "̠",
"_o": "̥",
"_u": "̮",
"_v": "̬",
"_{": "̼",
"_|": "̩",
"_~": "̰",
"_˔": "̝",
"_˕": "̞",
"_⊢": "̙",
"_⊣": "̘",
"a/": "ɐ",
"a|": "ɑ",
"b(": "ɓ",
"c,": "ç",
"c/": "ɔ",
"d(": "ɗ",
"d)": "ɖ",
"e-": "ɘ",
"e/": "ə",
"g(": "ɠ",
"h,": "ɦ",
"h-": "ħ",
"h/": "ɥ",
"h^": "ʰ",
"i-": "ɨ",
"j(": "ʄ",
"j,": "ʝ",
"j-": "ɟ",
"j^": "ʲ",
"k/": "ʞ",
"l)": "ɭ",
"l-": "ɬ",
"l^": "ˡ",
"lɾ": "ɺ",
"m,": "ɱ",
"m/": "ɯ",
"m^": "ᵐ",
"n)": "ɳ",
"n,": "ŋ",
"n^": "ⁿ",
"o*": "ʘ",
"o-": "ɵ",
"o.": "ʘ",
"o/": "ø",
"o^": "̊",
"o_": "̥",
"o|": "ɑ",
"r)": "ɽ",
"r/": "ɹ",
"r0": "ɾ",
"r>": "˞",
"rO": "ɾ",
"s)": "ʂ",
"t)": "ʈ",
"u-": "ʉ",
"u^": "̆",
"u_": "̮",
"v,": "ⱱ",
"v/": "ʌ",
"v0": "ʋ",
"vO": "ʋ",
"v^": "̌",
"v_": "̬",
"w,": "ɰ",
"w/": "ʍ",
"w^": "ʷ",
"w|": "ɰ",
"x,": "ɣ",
"x^": "̽",
"xʃ": "ɧ",
"y/": "ʎ",
"z)": "ʐ",
"{_": "̼",
"|*": "ǀ",
"|-": "ꜜ",
"|<": "⊣",
"|=": "ǂ",
"|>": "⊢",
"|A": "ɒ",
"|O": "ɒ",
"|^": "̍",
"|_": "̩",
"|a": "ɒ",
"|o": "ɒ",
"||": "‖",
"~^": "̃",
"~_": "̰",
"~~": "̴",
"ŋ,": "ɲ",
"ŋ^": "ᵑ",
"ŋᵑ": "^",
"ɑ/": "ɒ",
"ɒ/": "ɑ",
"ɖ(": "ᶑ",
"ɗ)": "ᶑ",
"ɛ/": "ɜ",
"ɜ(": "ɞ",
"ɜ/": "ɛ",
"ɞ(": "ɜ",
"ɟ(": "ʄ",
"ɢ(": "ʛ",
"ɣ,": "χ",
"ɣ^": "ˠ",
"ɣˠ": "^",
"ɥ^": "ᶣ",
"ɥᶣ": "^",
"ɧX": "ʃ",
"ɧx": "ʃ",
"ɧʃ": "x",
"ɲ,": "ŋ",
"ɹ)": "ɻ",
"ɺL": "ɾ",
"ɺl": "ɾ",
"ɺɾ": "l",
"ɻ)": "ɹ",
"ɽ)": "ɾ",
"ɾ)": "ɽ",
"ɾL": "ɺ",
"ɾl": "ɺ",
"ʀ/": "ʁ",
"ʁ/": "ʀ",
"ʃX": "ɧ",
"ʃx": "ɧ",
"ʄ(": "ɟ",
"ʋ^": "ᶹ",
"ʋᶹ": "^",
"ʔ-": "ʡ",
"ʔ/": "ʕ",
"ʕ-": "ʢ",
"ʕ/": "ʔ",
"ʕ^": "ˤ",
"ʕˤ": "^",
"ʛ(": "ɢ",
"ʡ-": "ʔ",
"ʢ-": "ʕ",
"ː-": "ˑ",
"ˑ-": "ː",
"˔_": "̝",
"˔̝": "_",
"˕_": "̞",
"˕̞": "_",
"ˠɣ": "^",
"ˤʕ": "^",
"̘⊣": "_",
"̙⊢": "_",
"̝˔": "_",
"̞˕": "_",
"͜)": "‿",
"χ,": "ɣ",
"ᵑŋ": "^",
"ᶑ(": "ɖ",
"ᶑ)": "ɗ",
"ᶣɥ": "^",
"ᶹʋ": "^",
"‿)": "͜",
"⊢_": "̙",
"⊢̙": "_",
"⊣_": "̘",
"⊣̘": "_"
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
import digraphs from './digraphs.json';
import { setSelectionRange, insertAtCursor } from '../../StackOverflow/inputCursorManagement.js';
export function usePhondueDigraphs(event) {
let val = event.target.value;
let pos = event.target.selectionStart || val.length;
const key = typeof event.which !== "undefined" ? event.which : event.keyCode,
digraph = digraphs[val.substr(pos - 1, 1) + String.fromCharCode(key)];
if (digraph) {
event.preventDefault();
insertAtCursor(event.target, digraph, -1);
}
}

View File

@ -0,0 +1,126 @@
<h1>Pronunciation Field Help</h1>
<p>The Pronunciation field in Lexiconga utilizes <a href='https://github.com/KeyboardFire/phondue' target='_blank'>KeyboardFire's
Phondue</a> web script to make typing IPA pronunciations much more convenient. While you
can still point and click to select IPA characters using the IPA table,
Phondue provides many two-key keyboard shortcuts called <strong>digraphs</strong>
that allow you to type any IPA symbol with only a standard keyboard.</p>
<p>These symbols were decided to be intuitive as possible:</p>
<ul>
<li>
<p>Many symbols that look similar to letters of the English alphabet (such as
small caps) can be produced by doubling the capital letter:</p>
<pre><code> GG &rarr; ɢ ?? &rarr; ʔ NN &rarr; ɴ BB &rarr; ʙ RR &rarr; ʀ XX &rarr; χ
LL &rarr; ʟ II &rarr; ɪ YY &rarr; ʏ UU &rarr; ʊ EE &rarr; ɛ OO &rarr; ɞ
AA &rarr; ɑ '' &rarr; ˈ ,, &rarr; ˌ :: &rarr; ː
</code></pre>
</li>
<li>
<p>The slash is used to &quot;reflect,&quot; &quot;rotate,&quot; or otherwise flip around a given
symbol:</p>
<pre><code> ʀ/ &rarr; ʁ ʔ/ &rarr; ʕ ?/ &rarr; ʕ r/ &rarr; ɹ y/ &rarr; ʎ m/ &rarr; ɯ
o/ &rarr; ø e/ &rarr; ə ɛ/ &rarr; ɜ c/ &rarr; ɔ a/ &rarr; ɐ ɑ/ &rarr; ɒ
w/ &rarr; ʍ h/ &rarr; ɥ k/ &rarr; ʞ !/ &rarr; ¡ v/ &rarr; ʌ
</code></pre>
<p>Also note that for any digraph, if either of the two characters that
compose it are not &quot;standard&quot; letters you can find on your keyboard, the
digraph can be done in reverse order—so since ʀ/ produces ʁ, you can press
/ again to go back to ʀ in case of accidental flippage (it's also weirdly
amusing to press RR///////).</p>
<p>This sometimes makes it easier to enter certain pairs; you may prefer to
use <code>AA</code> for <code>ɑ</code> and <code>AA/</code> for <code>ɒ</code> instead of the
visual <code>o|</code> and <code>|o</code>.</p>
</li>
<li>
<p>Digraphs have also been designed for when a symbol looks like two overlayed
characters, or two characters next to each other. This includes &quot;hooked&quot;
letters (such as ŋ), produced with the original letter and a comma, and
&quot;stroked&quot; letters (such as ɟ), produced with the original letter and a dash:</p>
<pre><code> m, &rarr; ɱ n, &rarr; ŋ ŋ, &rarr; ɲ v, &rarr; ⱱ c, &rarr; ç j, &rarr; ʝ
x, &rarr; ɣ ɣ, &rarr; χ h, &rarr; ɦ w, &rarr; ɰ
j- &rarr; ɟ h- &rarr; ħ l- &rarr; ɬ i- &rarr; ɨ u- &rarr; ʉ e- &rarr; ɘ
o- &rarr; ɵ ʕ- &rarr; ʢ ?- &rarr; ʡ ʔ- &rarr; ʡ
LZ &rarr; ɮ OX &rarr; ɤ XO &rarr; ɤ OE &rarr; œ EB &rarr; ɞ AE &rarr; æ
CE &rarr; ɶ RL &rarr; ɺ LR &rarr; ɺ ɾl &rarr; ɺ lɾ &rarr; ɺ
w| &rarr; ɰ o/ &rarr; ø ɜ( &rarr; ɞ /\ &rarr; ʌ o| &rarr; ɑ a| &rarr; ɑ
|o &rarr; ɒ |a &rarr; ɒ
o. &rarr; ʘ |= &rarr; ǂ || &rarr; ‖ /^ &rarr; ↗ /&gt; &rarr; ↗ \v &rarr;
\&gt; &rarr;
</code></pre>
</li>
<li>
<p>Others are based on pronunciation:</p>
<pre><code> PH &rarr; ɸ BH &rarr; β TH &rarr; θ DH &rarr; ð SH &rarr; ʃ ZH &rarr; ʒ
SJ &rarr; ɕ ZJ &rarr; ʑ ʃx &rarr; ɧ xʃ &rarr; ɧ
</code></pre>
<p>It may be worth noting at this point that digraphs of two lowercase letters
were intentionally avoided to prevent interference with regular typing. If
you need to type a sequence without it turning into a digraph (maybe you
want to type an actual <code>ʃx</code>), place a backslash between the two characters
(so, type <code>ʃ\x</code>).</p>
<p>In fact, [backslash][anything] is treated as a digraph that simply resolves
to the second character.</p>
</li>
<li>
<p>A few digraphs are based on shape:</p>
<pre><code> rO &rarr; ɾ r0 &rarr; ɾ vO &rarr; ʋ v0 &rarr; ʋ
</code></pre>
<p>Another related point: digraphs that contain a lowercase letter can also be
typed with that letter as uppercase. So, if <code>RO</code> is easier to type than
<code>rO</code>, that works as well.</p>
</li>
<li>
<p>Retroflex and nonpulmonic symbols have their own categories:</p>
<pre><code> retroflex: ) looks like the shape of the tongue
t) &rarr; ʈ d) &rarr; ɖ n) &rarr; ɳ r) &rarr; ɽ ɾ) &rarr; ɽ s) &rarr; ʂ
z) &rarr; ʐ ɹ) &rarr; ɻ l) &rarr; ɭ ɗ) &rarr;
clicks: clicking noise reminiscent of a *
o* &rarr; ʘ |* &rarr; ǀ !* &rarr; ǃ =* &rarr; ǁ
implosives and ejective marker: direction of airflow
b( &rarr; ɓ d( &rarr; ɗ j( &rarr; ʄ ɟ( &rarr; ʄ g( &rarr; ɠ ɢ( &rarr; ʛ
ɖ( &rarr; ᶑ ') &rarr; ʼ
</code></pre>
</li>
<li>
<p>Superscripts and diacritics that go above the letter use <code>^</code>, diacritics that
go below use <code>_</code>, and miscellaneous &quot;moved&quot; symbols
use <code>&lt;</code> or <code>&gt;</code>:</p>
<pre><code> ^h &rarr; ʰ ^n &rarr; ⁿ ^m &rarr; ᵐ ^ŋ &rarr; ᵑ ^l &rarr; ˡ ^w &rarr; ʷ
^j &rarr; ʲ ^ɥ &rarr; ᶣ ^ʋ &rarr; ᶹ ^ɣ &rarr; ˠ ^ʕ &rarr; ˤ
_| &rarr; ◌̩ ^| &rarr; ◌̍ _o &rarr; ◌̥ ^o &rarr; ◌̊ _v &rarr; ◌̬ ^v &rarr; ◌̌
_^ &rarr; ◌̯ _: &rarr; ◌̤ _~ &rarr; ◌̰ _[ &rarr; ◌̪ _] &rarr; ◌̺ _{ &rarr; ◌̼
_+ &rarr; ◌̟ __ &rarr; ◌̠ _) &rarr; ◌̹ _( &rarr; ◌̜ _# &rarr; ◌̻ [] &rarr; ◌̻
^&gt; &rarr; ◌̚ ^: &rarr; ◌̈ ^x &rarr; ◌̽ ^~ &rarr; ◌̃
-' &rarr; ˔ _˔ &rarr; ◌̝ -, &rarr; ˕ _˕ &rarr; ◌̞ &lt;| &rarr; ⊣ _⊣ &rarr; ◌̘
&gt;| &rarr; ⊢ _⊢ &rarr; ◌̙
~~ &rarr; ◌̴ &gt;r &rarr; ˞
</code></pre>
<p>Note that all of these sequences can be flipped in order—that is, <code>^h</code>
produces the same thing as <code>h^</code>. This allows usage such as <code>|&lt;_</code> to produce
<code>⊣_</code> which becomes ◌̘.</p>
</li>
<li>
<p>Tonal countours use numbers plus <code>|</code>:</p>
<pre><code> 5| &rarr; ˥ 4| &rarr; ˦ 3| &rarr; ˧ 2| &rarr; ˨ 1| &rarr; ˩
+| &rarr; ꜛ -| &rarr;
</code></pre>
</li>
<li>
<p>Finally, some digraphs simply have unique mnemonics:</p>
<pre><code> ː- &rarr; ˑ &quot;chop off&quot; the bottom triangle
(( &rarr; ◌͡◌ two parens for a tie that connects 2 chars
)) &rarr; ◌͜◌
◌͜◌) &rarr; ‿ one more paren to make it a little bit longer
</code></pre>
</li>
</ul>

View File

@ -0,0 +1,29 @@
// https://stackoverflow.com/questions/4825683/how-do-i-create-and-read-a-value-from-cookie/4825695#4825695
export function setCookie (name, value, days) {
let expires;
if (days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = '; expires=' + date.toGMTString();
}
else {
expires = '';
}
document.cookie = name + '=' + value + expires + '; path=/';
}
export function getCookie(c_name) {
if (document.cookie.length > 0) {
let c_start = document.cookie.indexOf(c_name + '=');
if (c_start != -1) {
c_start = c_start + c_name.length + 1;
let c_end = document.cookie.indexOf(';', c_start);
if (c_end == -1) {
c_end = document.cookie.length;
}
return unescape(document.cookie.substring(c_start, c_end));
}
}
return '';
}

View File

@ -0,0 +1,81 @@
export function insertAtCursor(myField, myValue, adjustStart = 0) {
// http://stackoverflow.com/questions/11076975/insert-text-into-textarea-at-cursor-position-javascript
// IE support
if (document.selection) {
myField.focus();
sel = document.selection.createRange();
sel.text = myValue;
}
// MOZILLA and others
else if (myField.selectionStart || myField.selectionStart == '0') {
const selection = getInputSelection(myField);
selection.start += adjustStart;
myField.value = myField.value.substring(0, selection.start)
+ myValue
+ myField.value.substring(selection.end, myField.value.length);
myField.selectionStart = selection.start + myValue.length;
myField.selectionEnd = selection.start + myValue.length;
} else {
myField.value += myValue;
}
setSelectionRange(myField, myField.selectionEnd, myField.selectionEnd);
}
export function getInputSelection(el) {
// Retrieved from http://stackoverflow.com/a/4207763
var start = 0, end = 0, normalizedValue, range,
textInputRange, len, endRange;
el.focus();
if (typeof el.selectionStart == "number" && typeof el.selectionEnd == "number") {
start = el.selectionStart;
end = el.selectionEnd;
} else {
range = document.selection.createRange();
if (range && range.parentElement() == el) {
len = el.value.length;
normalizedValue = el.value.replace(/\r\n/g, "\n");
// Create a working TextRange that lives only in the input
textInputRange = el.createTextRange();
textInputRange.moveToBookmark(range.getBookmark());
// Check if the start and end of the selection are at the very end
// of the input, since moveStart/moveEnd doesn't return what we want
// in those cases
endRange = el.createTextRange();
endRange.collapse(false);
if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
start = end = len;
} else {
start = -textInputRange.moveStart("character", -len);
start += normalizedValue.slice(0, start).split("\n").length - 1;
if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) {
end = len;
} else {
end = -textInputRange.moveEnd("character", -len);
end += normalizedValue.slice(0, end).split("\n").length - 1;
}
}
}
}
return { start, end };
}
export function setSelectionRange(input, selectionStart, selectionEnd) {
// Retrieved from http://stackoverflow.com/a/17858641/3508346
if (input.setSelectionRange) {
input.focus();
input.setSelectionRange(selectionStart, selectionEnd);
}
else if (input.createTextRange) {
var range = input.createTextRange();
range.collapse(true);
range.moveEnd('character', selectionEnd);
range.moveStart('character', selectionStart);
range.select();
}
}

View File

@ -0,0 +1,118 @@
/*
Retrieved from http://stackoverflow.com/a/18391901/3508346
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const defaultDiacriticsRemovalap = [
{ 'base': 'A', 'letters': '\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F' },
{ 'base': 'AA', 'letters': '\uA732' },
{ 'base': 'AE', 'letters': '\u00C6\u01FC\u01E2' },
{ 'base': 'AO', 'letters': '\uA734' },
{ 'base': 'AU', 'letters': '\uA736' },
{ 'base': 'AV', 'letters': '\uA738\uA73A' },
{ 'base': 'AY', 'letters': '\uA73C' },
{ 'base': 'B', 'letters': '\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181' },
{ 'base': 'C', 'letters': '\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E' },
{ 'base': 'D', 'letters': '\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779' },
{ 'base': 'DZ', 'letters': '\u01F1\u01C4' },
{ 'base': 'Dz', 'letters': '\u01F2\u01C5' },
{ 'base': 'E', 'letters': '\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E' },
{ 'base': 'F', 'letters': '\u0046\u24BB\uFF26\u1E1E\u0191\uA77B' },
{ 'base': 'G', 'letters': '\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E' },
{ 'base': 'H', 'letters': '\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D' },
{ 'base': 'I', 'letters': '\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197' },
{ 'base': 'J', 'letters': '\u004A\u24BF\uFF2A\u0134\u0248' },
{ 'base': 'K', 'letters': '\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2' },
{ 'base': 'L', 'letters': '\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780' },
{ 'base': 'LJ', 'letters': '\u01C7' },
{ 'base': 'Lj', 'letters': '\u01C8' },
{ 'base': 'M', 'letters': '\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C' },
{ 'base': 'N', 'letters': '\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4' },
{ 'base': 'NJ', 'letters': '\u01CA' },
{ 'base': 'Nj', 'letters': '\u01CB' },
{ 'base': 'O', 'letters': '\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C' },
{ 'base': 'OI', 'letters': '\u01A2' },
{ 'base': 'OO', 'letters': '\uA74E' },
{ 'base': 'OU', 'letters': '\u0222' },
{ 'base': 'OE', 'letters': '\u008C\u0152' },
{ 'base': 'oe', 'letters': '\u009C\u0153' },
{ 'base': 'P', 'letters': '\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754' },
{ 'base': 'Q', 'letters': '\u0051\u24C6\uFF31\uA756\uA758\u024A' },
{ 'base': 'R', 'letters': '\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782' },
{ 'base': 'S', 'letters': '\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784' },
{ 'base': 'T', 'letters': '\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786' },
{ 'base': 'TZ', 'letters': '\uA728' },
{ 'base': 'U', 'letters': '\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244' },
{ 'base': 'V', 'letters': '\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245' },
{ 'base': 'VY', 'letters': '\uA760' },
{ 'base': 'W', 'letters': '\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72' },
{ 'base': 'X', 'letters': '\u0058\u24CD\uFF38\u1E8A\u1E8C' },
{ 'base': 'Y', 'letters': '\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE' },
{ 'base': 'Z', 'letters': '\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762' },
{ 'base': 'a', 'letters': '\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250' },
{ 'base': 'aa', 'letters': '\uA733' },
{ 'base': 'ae', 'letters': '\u00E6\u01FD\u01E3' },
{ 'base': 'ao', 'letters': '\uA735' },
{ 'base': 'au', 'letters': '\uA737' },
{ 'base': 'av', 'letters': '\uA739\uA73B' },
{ 'base': 'ay', 'letters': '\uA73D' },
{ 'base': 'b', 'letters': '\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253' },
{ 'base': 'c', 'letters': '\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184' },
{ 'base': 'd', 'letters': '\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A' },
{ 'base': 'dz', 'letters': '\u01F3\u01C6' },
{ 'base': 'e', 'letters': '\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD' },
{ 'base': 'f', 'letters': '\u0066\u24D5\uFF46\u1E1F\u0192\uA77C' },
{ 'base': 'g', 'letters': '\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F' },
{ 'base': 'h', 'letters': '\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265' },
{ 'base': 'hv', 'letters': '\u0195' },
{ 'base': 'i', 'letters': '\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131' },
{ 'base': 'j', 'letters': '\u006A\u24D9\uFF4A\u0135\u01F0\u0249' },
{ 'base': 'k', 'letters': '\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3' },
{ 'base': 'l', 'letters': '\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747' },
{ 'base': 'lj', 'letters': '\u01C9' },
{ 'base': 'm', 'letters': '\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F' },
{ 'base': 'n', 'letters': '\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5' },
{ 'base': 'nj', 'letters': '\u01CC' },
{ 'base': 'o', 'letters': '\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275' },
{ 'base': 'oi', 'letters': '\u01A3' },
{ 'base': 'ou', 'letters': '\u0223' },
{ 'base': 'oo', 'letters': '\uA74F' },
{ 'base': 'p', 'letters': '\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755' },
{ 'base': 'q', 'letters': '\u0071\u24E0\uFF51\u024B\uA757\uA759' },
{ 'base': 'r', 'letters': '\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783' },
{ 'base': 's', 'letters': '\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B' },
{ 'base': 't', 'letters': '\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787' },
{ 'base': 'tz', 'letters': '\uA729' },
{ 'base': 'u', 'letters': '\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289' },
{ 'base': 'v', 'letters': '\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C' },
{ 'base': 'vy', 'letters': '\uA761' },
{ 'base': 'w', 'letters': '\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73' },
{ 'base': 'x', 'letters': '\u0078\u24E7\uFF58\u1E8B\u1E8D' },
{ 'base': 'y', 'letters': '\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF' },
{ 'base': 'z', 'letters': '\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763' }
];
const diacriticsMap = {};
for (let i = 0; i < defaultDiacriticsRemovalap.length; i++) {
const letters = defaultDiacriticsRemovalap[i].letters;
for (let j = 0; j < letters.length; j++) {
diacriticsMap[letters[j]] = defaultDiacriticsRemovalap[i].base;
}
}
// "what?" version ... http://jsperf.com/diacritics/12
export default function removeDiacritics(str) {
return str.replace(/[^\u0000-\u007E]/g, function (a) {
return diacriticsMap[a] || a;
});
}

View File

@ -0,0 +1 @@
export const DELETED_WORDS_LOCALSTORAGE_KEY = 'deletedWords';

View File

@ -0,0 +1,51 @@
import { clearDictionary, saveDictionary } from "../dictionaryManagement";
import { uploadWholeDictionary, performSync } from "./sync";
import { request } from "./helpers";
import { saveToken } from "./utilities";
import { addMessage, hideAllModals } from "../utilities";
import { renderAll } from "../render";
import { renderDeletedDictionaryChangeModal, renderChangeDictionaryOptions } from "./render";
export function createNewDictionary() {
clearDictionary();
saveDictionary();
renderAll();
uploadWholeDictionary(true);
hideAllModals();
addMessage('New Dictionary Created!');
}
export function changeDictionary(dictionary) {
dictionary = typeof dictionary.target !== 'undefined' ? dictionary.target.value : dictionary;
if (dictionary !== window.currentDictionary.externalID) {
addMessage('Loading Dictionary...');
request({
action: 'change-dictionary',
dictionary,
}, successData => {
saveToken(successData.token);
performSync(successData.dictionary);
hideAllModals();
}, error => {
console.error(error);
addMessage(error, undefined, 'error');
});
}
}
export function updateCurrentChangeDictionaryOption() {
const label = window.currentDictionary.name + ' ' + window.currentDictionary.specification;
document.getElementById('accountSettingsChangeDictionary')
.querySelector(`option[value="${window.currentDictionary.externalID}"]`).innerText = label;
}
export function deleteDictionary(deletedId) {
request({
action: 'delete-current-dictionary',
}, successful => {
if (successful) {
renderChangeDictionaryOptions();
renderDeletedDictionaryChangeModal(deletedId);
}
})
}

16
src/js/account/helpers.js Normal file
View File

@ -0,0 +1,16 @@
export function request (data = {}, success = () => {}, error = () => {}/* , fail = () => {} */) {
return fetch('./api/', {
method: 'POST', // or 'PUT'
body: JSON.stringify(data), // data can be `string` or {object}!
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin',
}).then(res => res.json())
.then(response => {
if (response.error) {
return error(response.data);
}
return success(response.data);
});
}

72
src/js/account/index.js Normal file
View File

@ -0,0 +1,72 @@
import '../../scss/Account/main.scss';
import { renderLoginForm } from "./render";
import { validateToken, updateAccountData } from './login';
import {
uploadWords,
uploadDetails,
uploadWholeDictionary,
deleteWords
} from './sync';
import { saveDeletedWordLocally } from './utilities';
import { addMessage } from '../utilities';
import { updateCurrentChangeDictionaryOption, deleteDictionary } from './dictionaryManagement';
export function showLoginForm() {
renderLoginForm();
}
export function loginWithToken() {
validateToken();
}
export function editAccount(accountData) {
updateAccountData(accountData);
}
export function syncImportedDictionary() {
uploadWholeDictionary(true);
}
export function uploadDetailsDirect() {
uploadDetails().catch(err => {
console.error(err);
addMessage('Could not connect to account. Trying again in 10 seconds.', undefined, 'error');
setTimeout(() => {
uploadDetails();
}, 10000);
});
}
export function uploadWord(word) {
uploadWords([word]).catch(err => {
console.error(err);
addMessage('Could not connect to account. Trying again in 10 seconds.', undefined, 'error');
setTimeout(() => {
uploadWord(word);
}, 10000);
});
}
export function syncImportedWords(words) {
uploadWords(words);
}
export function deleteWord(wordId) {
deleteWords([wordId]).catch(err => {
console.error(err);
saveDeletedWordLocally(wordId);
addMessage('Could not connect to account. Trying again in 10 seconds.', undefined, 'error');
setTimeout(() => {
deleteWord(wordId);
}, 10000);
});
}
export function updateChangeDictionaryOption() {
updateCurrentChangeDictionaryOption();
}
export function deleteCurrentDictionary(deletedId) {
deleteDictionary(deletedId);
}

157
src/js/account/login.js Normal file
View File

@ -0,0 +1,157 @@
import { request } from "./helpers";
import { saveToken } from "./utilities";
import { addMessage } from "../utilities";
import { setupLogoutButton } from "./setupListeners";
import { renderAccountSettings, renderAccountActions, renderMakePublic } from "./render";
import { uploadWholeDictionary, syncDictionary } from "./sync";
import { setCookie } from "../StackOverflow/cookie";
export function logIn() {
const email = document.getElementById('loginEmail').value.trim(),
password = document.getElementById('loginPassword').value;
const loginErrorMessages = document.getElementById('loginErrorMessages');
let errorHTML = '';
if (email === '') {
errorHTML += '<p class="bold red">Please enter your email address.</p>';
}
if (password === '') {
errorHTML += '<p class="bold red">Please enter your password.</p>';
}
loginErrorMessages.innerHTML = errorHTML;
if (errorHTML === '') {
request({
action: 'login',
email,
password,
}, successData => {
saveToken(successData.token);
window.account = successData.user;
}, errorData => {
errorHTML += '<p class="bold red">' + errorData + '</p>';
}).then(() => {
loginErrorMessages.innerHTML = errorHTML;
if (errorHTML === '') {
const loginModal = document.getElementById('loginModal');
loginModal.parentElement.removeChild(loginModal);
triggerLoginChanges();
addMessage(`Welcome${window.account.publicName !== '' ? ', ' + window.account.publicName : ''}! You are logged in.`, 0);
syncDictionary();
}
}).catch(err => console.error(err));
}
}
export function createAccount() {
const email = document.getElementById('createNewEmail').value.trim(),
password = document.getElementById('createNewPassword').value,
confirm = document.getElementById('createNewConfirm').value.trim(),
publicName = document.getElementById('createNewPublicName').value.trim(),
allowEmail = document.getElementById('createNewAllowEmails').checked;
const createAccountErrorMessages = document.getElementById('createAccountErrorMessages');
let errorHTML = '';
if (email === '') {
errorHTML += '<p class="bold red">Please enter an email address.</p>';
} else if (!/.+@.+\..+/.test(email)) {
errorHTML += '<p class="bold red">Please double-check your email address.</p>';
}
if (password === '') {
errorHTML += '<p class="bold red">Please enter a password.</p>';
} else if (confirm !== password) {
errorHTML += '<p class="bold red">The password you entered to confirm did not match the password you entered.</p>';
}
createAccountErrorMessages.innerHTML = errorHTML;
if (errorHTML === '') {
request({
action: 'check-email',
email,
}, emailExists => {
if (emailExists) {
errorHTML += '<p class="bold red">The email address you entered already exists.</p>';
}
}, errorData => {
console.error(errorData);
errorHTML += `<p class="bold red">${errorData}</p>`;
}).then(() => {
createAccountErrorMessages.innerHTML = errorHTML;
if (errorHTML === '') {
request({
action: 'create-account',
email,
password,
userData: {
publicName,
allowEmail,
},
}, responseData => {
saveToken(responseData.token);
window.account = responseData.user;
if (responseData.hasOwnProperty('dictionary')) {
uploadWholeDictionary(); // Saves external id
}
}, errorData => {
errorHTML += `<p class="bold red">${errorData}</p>`;
}).then(() => {
createAccountErrorMessages.innerHTML = errorHTML;
if (errorHTML === '') {
const loginModal = document.getElementById('loginModal');
loginModal.parentElement.removeChild(loginModal);
triggerLoginChanges();
addMessage('Account Created Successfully!');
addMessage(`Welcome${publicName !== '' ? ', ' + publicName : ''}! You are logged in.`, 0);
if (window.currentDictionary.hasOwnProperty('externalID')) {
// Ensure dictionary uploads to overwrite the auto-created default dictionary
delete window.currentDictionary.externalID;
}
syncDictionary(false);
}
});
}
}).catch(err => console.error(err));
}
}
export function validateToken() {
request({
action: 'validate-token',
}, userData => {
window.account = userData;
triggerLoginChanges();
addMessage(`Welcome${window.account.publicName !== '' ? ', ' + window.account.publicName : ''}! You are logged in.`, 0);
syncDictionary();
}, error => {
addMessage(error + '. Logging Out.', undefined, 'error');
setCookie('token', '', -1);
});
}
export function triggerLoginChanges() {
const loginButton = document.getElementById('loginCreateAccountButton');
const logoutButton = document.createElement('a');
logoutButton.classList.add('button');
logoutButton.id = 'logoutButton';
logoutButton.innerHTML = 'Log Out';
loginButton.parentElement.appendChild(logoutButton);
loginButton.parentElement.removeChild(loginButton);
setupLogoutButton(logoutButton);
renderMakePublic();
renderAccountSettings();
renderAccountActions();
}
export function updateAccountData(userData) {
request({
action: 'set-user-data',
userData,
}, successData => {
document.getElementById('accountSettingsNewPassword').value = '';
addMessage('Successfully Updated Account Data');
}, error => {
addMessage(error, undefined, 'error');
});
}

View File

@ -0,0 +1,113 @@
import { request } from "./helpers";
export function renderForgotPasswordForm() {
const modal = document.createElement('section');
modal.classList.add('modal');
modal.innerHTML = `<div class="modal-background"></div>
<div class="modal-content">
<a class="close-button">&times;&#xFE0E;</a>
<section class="info-modal" id="forgotPasswordForm">
<h2>Forgot Password</h2>
<p>Enter the email address associated with your Lexiconga account to initiate a password reset.</p>
<label>Email<br>
<input type="email" id="forgotPasswordEmailField" style="max-width:250px;" maxlength="100">
</label>
<section id="forgotPasswordErrorMessages"></section>
<button class="button" id="forgotPasswordSubmit">Email Password Reset Key</button>
</section>
</div>`;
document.body.appendChild(modal);
setupStartResetForm();
setupInfoModal(modal);
}
function setupInfoModal(modal) {
const closeElements = modal.querySelectorAll('.modal-background, .close-button');
Array.from(closeElements).forEach(close => {
close.addEventListener('click', () => {
modal.parentElement.removeChild(modal);
});
});
}
function setupStartResetForm() {
document.getElementById('forgotPasswordSubmit').addEventListener('click', startPasswordReset);
}
function startPasswordReset() {
const email = document.getElementById('forgotPasswordEmailField').value.trim();
const errorMessageElement = document.getElementById('forgotPasswordErrorMessages');
let errorMessage = '';
if (email === '') {
errorMessage += '<p class="red bold">Please enter an email address.</p>';
}
errorMessageElement.innerHTML = errorMessage;
if (errorMessage === '') {
request({
action: 'initiate-password-reset',
email,
}, success => {
console.log(success);
}, error => {
errorMessage += '<p class="red bold">' + error + '</p>';
}).then(() => {
errorMessageElement.innerHTML = errorMessage;
if (errorMessage === '') {
document.getElementById('forgotPasswordForm').innerHTML = `<h2>Password Reset Key Sent</h2>
<p>Go check your email for the password reset link.</p>
<p><em>Note that it may be sent to your spam/junk folder by mistake.</em></p>`;
}
});
}
}
function setupPasswordResetForm() {
const submitButton = document.getElementById('newPasswordSubmit');
if (submitButton) {
submitButton.addEventListener('click', submitPasswordReset);
}
}
function submitPasswordReset() {
const password = document.getElementById('newPassword').value;
const confirm = document.getElementById('newConfirm').value;
const account = document.getElementById('account').value;
const errorMessageElement = document.getElementById('newPasswordErrorMessages');
let errorMessage = '';
if (password === '') {
errorMessage += '<p class="red bold">Please enter a password.</p>';
} else if (password !== confirm) {
errorMessage += '<p class="red bold">The passwords do not match.</p>';
}
errorMessageElement.innerHTML = errorMessage;
if (errorMessage === '') {
request({
action: 'password-reset',
account,
password,
}, success => {
console.log(success);
}, error => {
errorMessage += '<p class="red bold">' + error + '</p>';
}).then(() => {
errorMessageElement.innerHTML = errorMessage;
if (errorMessage === '') {
document.getElementById('detailsPanel').innerHTML = `<h3>Your password has been reset</h3>
<p>You can now <a href="/">Return to Lexiconga</a> and log in using your new password.</p>`;
}
});
}
}
window.onload = (function (oldLoad) {
oldLoad && oldLoad();
setTimeout(setupPasswordResetForm, 1000);
})(window.onload);

159
src/js/account/render.js Normal file
View File

@ -0,0 +1,159 @@
import { setupLoginModal, setupChangeDictionary, setupCreateNewDictionary, setupDeletedDictionaryChangeModal, setupMakePublic } from "./setupListeners";
import { getPublicLink } from "./utilities";
import { request } from "./helpers";
export function renderLoginForm() {
const loginModal = document.createElement('section');
loginModal.classList.add('modal');
loginModal.id = 'loginModal';
loginModal.innerHTML = `<div class="modal-background"></div>
<div class="modal-content"><a class="close-button">&times;&#xFE0E;</a>
<section>
<div class="split two">
<div>
<h2>Log In</h2>
<label>Email<br>
<input type="email" required id="loginEmail" maxlength="100">
</label>
<label>Password<br>
<input type="password" required id="loginPassword" maxlength="100">
</label>
<section id="loginErrorMessages"></section>
<button id="loginSubmit" class="button">Log In</button><br>
<a id="forgotPasswordButton" class="small button">Forgot Password?</a>
</div>
<div>
<h2>Create a New Account</h2>
<p>Creating an account allows you to save and switch between as many dictionaries as you need and access them from any device for free! If you have a dictionary you've been working on loaded already, it will automatically be uploaded to your account when you log in for the first time.</p>
<p>By creating an account, you are indicating that you agree to the Terms of Service and that you understand Lexiconga's Privacy Policy.</p>
<label>Email<br>
<input type="email" id="createNewEmail" maxlength="100">
</label>
<label>Password<br>
<input type="password" id="createNewPassword" maxlength="100">
</label>
<label>Confirm Password<br>
<input type="password" id="createNewConfirm" maxlength="100">
</label>
<label>Public Name<br>
<input type="text" id="createNewPublicName" maxlength="50">
</label>
<label>Allow Emails
<input type="checkbox" id="createNewAllowEmails">
</label>
<p><small>If you allow emails, you'll be informed if any of our policies change for any reason. We'll <em>never</em> spam you or sell your information.</small></p>
<p><strong>Remember:</strong> Creating an account is <em>not</em> required to use Lexiconga's core features. Click "Help" in the site footer to learn what accounts provide.</p>
<section id="createAccountErrorMessages"></section>
<button id="createAccountSubmit" class="button">Create Account</button>
</div>
</div>
</section>
</div>`;
document.body.appendChild(loginModal);
setupLoginModal(loginModal);
}
export function renderMakePublic() {
const editSettingsTab = document.getElementById('editSettingsTab');
const { isPublic } = window.currentDictionary.settings;
let waitForSync = setInterval(() => {
if (window.currentDictionary.hasOwnProperty('externalID') && !isNaN(window.currentDictionary.externalID)) {
clearInterval(waitForSync);
const editSettingsTabHTML = `<label>Make Public
<input type="checkbox" id="editIsPublic"${isPublic ? ' checked' : ''}><br>
<small>Checking this box will make this public via a link you can share with others.</small>
</label>
<p id="publicLinkDisplay" style="${!isPublic ? 'display:none;': ''}margin-left:20px;">
<strong>Public Link:</strong><br>
<input readonly id="publicLink" value="${getPublicLink()}">
<a class="small button" id="publicLinkCopy">Copy</a>
</p>
`;
editSettingsTab.innerHTML += editSettingsTabHTML;
setupMakePublic();
}
}, 100);
}
export function renderAccountSettings() {
const accountSettingsColumn = document.getElementById('accountSettings');
const accountSettingsHTML = `<h3>Account Settings</h3>
<label>Email Address<br><input id="accountSettingsEmail" required maxlength="100" value="${window.account.email}"></label>
<label>Public Name<br><input id="accountSettingsPublicName" placeholder="Someone" maxlength="50" value="${window.account.publicName}"></label>
<label>Allow Emails <input type="checkbox" id="accountSettingsAllowEmails"${window.account.allowEmails ? ' checked' : ''}></label>
<label>New Password <small>Only fill if changing!</small><br><input type="password" id="accountSettingsNewPassword" placeholder="Leave Blank to Prevent Change"></label>
`;
accountSettingsColumn.innerHTML = accountSettingsHTML;
}
export function renderAccountActions() {
const accountActionsColumn = document.getElementById('accountActions');
const accountActionsHTML = `<h3>Account Actions</h3>
<label>Change Dictionary<br><select id="accountSettingsChangeDictionary"></select></label>
<p><a class="button" id="accountSettingsCreateNewDictionary">Create New Dictionary</a></p>
<details>
<summary><h4>Request Your Data</h4></summary>
<p>
Per your <a href="https://www.eugdpr.org/" target="_blank">GDPR</a> rights in Articles 1315 and 20, we allow you to request any and all data we have stored about you. The only data we have about you personally is your email address and your Public Name, if you decided to set one. All other data (your Dictionary data) is visible and accessible via the Export button under your Dictionary's Settings. Send an email to help@lexicon.ga to request your information.
</p>
</details>
<details>
<summary><h4>Delete Your Account</h4></summary>
<p>
Per your <a href="https://www.eugdpr.org/" target="_blank">GDPR</a> rights in Articles 17, if you wish for your account to be deleted, please contact us at help@lexicon.ga, and we will delete your account and all associated dictionaries and words as quickly as possible. Note that you can delete dictionaries yourself via your Dictionary's Settings.
</p>
<p>
Anything that is deleted from our system is permanently and irretrievably removed from our system and cannot be restored, though search engines or internet archives may retain a cached version of your content (there is nothing we can do about this, and you will need to seek out removal of that information by directly contacting the services that are caching your data).
</p>
</details>
`;
accountActionsColumn.innerHTML = accountActionsHTML;
renderChangeDictionaryOptions();
setupCreateNewDictionary();
}
export function renderChangeDictionaryOptions() {
request({
action: 'get-all-dictionary-names',
}, dictionaries => {
const changeDictionarySelect = document.getElementById('accountSettingsChangeDictionary');
const optionsHTML = dictionaries.map(dictionary => `<option value="${dictionary.id}">${dictionary.name}</option>`).join('');
changeDictionarySelect.innerHTML = optionsHTML;
changeDictionarySelect.value = window.currentDictionary.externalID;
setupChangeDictionary();
}, error => console.error(error));
}
export function renderDeletedDictionaryChangeModal(deletedId) {
const changeDictionarySelect = document.getElementById('accountSettingsChangeDictionary');
const lazyFilterOptions = changeDictionarySelect.querySelectorAll(`option:not([value="${deletedId}"])`);
const lazyFilter = document.createElement('select');
lazyFilter.innerHTML = '<option></option>';
lazyFilterOptions.forEach(option => {
lazyFilter.appendChild(option);
});
const otherDictionariesHTML = lazyFilter.innerHTML;
const modal = document.createElement('section');
modal.classList.add('modal');
modal.innerHTML = `<div class="modal-background"></div>
<div class="modal-content">
<section class="info-modal">
<h2>Dictionary Deleted</h2>
${lazyFilterOptions.length < 1 ? ''
: `<label>Select dictionary to load<br>
<select id="selectDictionaryToLoad">${otherDictionariesHTML}</select>
</label>
<p>OR</p>`}
<p><a class="button" id="createNewDictionaryAfterDelete">Create a New Dictionary</a></p>
</section>
</div>`;
document.body.appendChild(modal);
setupDeletedDictionaryChangeModal();
}

View File

@ -0,0 +1,78 @@
import { logIn, createAccount } from "./login";
import { setCookie } from "../StackOverflow/cookie";
import { changeDictionary, createNewDictionary } from "./dictionaryManagement";
import { addMessage } from "../utilities";
import { renderForgotPasswordForm } from "./passwordReset";
import { setupMaximizeButtons } from "../setupListeners/buttons";
export function setupLoginModal(modal) {
const closeElements = modal.querySelectorAll('.modal-background, .close-button');
Array.from(closeElements).forEach(close => {
close.addEventListener('click', () => {
modal.parentElement.removeChild(modal);
});
});
[
document.getElementById('loginEmail'),
document.getElementById('loginPassword'),
].forEach(field => {
field.addEventListener('keydown', event => {
if (['Enter', 'Return'].includes(event.key)) {
logIn();
}
});
});
[
document.getElementById('createNewEmail'),
document.getElementById('createNewPassword'),
document.getElementById('createNewConfirm'),
document.getElementById('createNewPublicName'),
].forEach(field => {
field.addEventListener('keydown', event => {
if (['Enter', 'Return'].includes(event.key)) {
createAccount();
}
});
});
document.getElementById('loginSubmit').addEventListener('click', logIn);
document.getElementById('forgotPasswordButton').addEventListener('click', renderForgotPasswordForm);
document.getElementById('createAccountSubmit').addEventListener('click', createAccount);
}
export function setupLogoutButton(logoutButton) {
logoutButton.addEventListener('click', () => {
setCookie('token', '', -1);
window.location.reload();
});
}
export function setupChangeDictionary() {
document.getElementById('accountSettingsChangeDictionary').addEventListener('change', changeDictionary);
}
export function setupCreateNewDictionary() {
document.getElementById('accountSettingsCreateNewDictionary').addEventListener('click', createNewDictionary);
}
export function setupDeletedDictionaryChangeModal() {
const selectDictionaryToLoad = document.getElementById('selectDictionaryToLoad')
if (selectDictionaryToLoad) {
selectDictionaryToLoad.addEventListener('change', changeDictionary);
}
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);
});
setupMaximizeButtons();
}

231
src/js/account/sync.js Normal file
View File

@ -0,0 +1,231 @@
import { addMessage } from "../utilities";
import { saveDictionary, clearDictionary } from "../dictionaryManagement";
import { request } from "./helpers";
import { saveToken, dictionaryIsDefault, getPublicLink } from "./utilities";
import { renderAll } from "../render";
import { sortWords } from "../wordManagement";
import { getLocalDeletedWords, clearLocalDeletedWords, saveDeletedWordsLocally } from "./utilities";
import { renderChangeDictionaryOptions } from "./render";
export function syncDictionary(uploadAsNewIfNoExternalID = true) {
if (!window.currentDictionary.hasOwnProperty('externalID') && !dictionaryIsDefault()) {
uploadWholeDictionary(uploadAsNewIfNoExternalID);
} else {
addMessage('Syncing...');
request({
action: 'get-current-dictionary',
}, remote => {
performSync(remote);
}, error => {
console.error(error);
}).catch(err => console.error(err));
}
}
export function performSync(remoteDictionary) {
if (remoteDictionary.details.externalID !== window.currentDictionary.externalID) {
clearDictionary();
}
const detailsSynced = syncDetails(remoteDictionary.details);
if (detailsSynced === false) {
addMessage('Could not sync', 10000, 'error');
} else {
detailsSynced.then(success => {
renderAll();
if (success) {
syncWords(remoteDictionary.words, remoteDictionary.deletedWords).then(success => {
if (success) {
renderAll();
document.getElementById('accountSettingsChangeDictionary').value = window.currentDictionary.externalID;
if (document.getElementById('publicLink')) {
document.getElementById('publicLink').value = getPublicLink();
}
if (document.getElementById('publicLinkDisplay')) {
document.getElementById('publicLinkDisplay').style.display = window.currentDictionary.settings.isPublic ? '' : 'none';
}
} else {
console.error('word sync failed');
}
});
} else {
console.error('details sync failed');
}
});
}
}
export function uploadWholeDictionary(asNew = false) {
let promise;
if (asNew) {
promise = request({
action: 'create-new-dictionary',
}, successData => {
saveToken(successData.token);
}, errorData => {
console.error(errorData);
});
} else {
promise = Promise.resolve();
}
const dictionary = {
details: Object.assign({}, window.currentDictionary),
words: window.currentDictionary.words,
};
delete dictionary.details.words; // Ugly way to easily get the data I need.
promise.then(() => {
request({
action: 'set-whole-current-dictionary',
dictionary,
}, remoteId => {
window.currentDictionary.externalID = remoteId;
if (document.getElementById('publicLink')) {
document.getElementById('publicLink').value = getPublicLink();
}
saveDictionary(false);
addMessage('Dictionary Uploaded Successfully');
renderChangeDictionaryOptions();
}, errorData => {
console.error(errorData);
addMessage(errorData, 10000, 'error');
})
.catch(err => console.error('set-whole-current-dictionary: ', err));
})
.catch(err => console.error('create-new-dictionary: ', err));
}
export function syncDetails(remoteDetails = false) {
let direction; // This is if/else if tree the only way I can think to correctly prioritize this when to upload vs download.
if (remoteDetails === false) {
direction = 'up';
} else if (!window.currentDictionary.hasOwnProperty('externalID')) { // If mismatched id, dictionary will be cleared, allowing it to be overwritten
direction = 'down';
} else if (remoteDetails.lastUpdated < window.currentDictionary.lastUpdated) {
direction = 'up';
} else if (remoteDetails.lastUpdated > window.currentDictionary.lastUpdated) {
direction = 'down';
}
if (direction === 'up') {
return uploadDetails();
} else if (direction === 'down') {
window.currentDictionary = Object.assign(window.currentDictionary, remoteDetails);
saveDictionary(false);
}
addMessage('Dictionary details synchronized');
return Promise.resolve(true);
}
export function uploadDetails() {
const details = Object.assign({}, window.currentDictionary);
delete details.words;
return request({
action: 'set-dictionary-details',
details,
}, successful => {
addMessage('Saved Details to Server');
return successful;
}, error => {
console.error(error);
addMessage('Could not sync dictionary', 10000, 'undefined');
return false;
});
}
export function syncWords(remoteWords, deletedWords) {
const words = window.currentDictionary.words.filter(word => {
const deleted = deletedWords.find(deletedWord => deletedWord.id === word.wordId);
if (deleted) {
return deleted.deletedOn < word.createdOn;
}
return true;
});
const localWordsToUpload = words.filter(word => {
// Find words that don't exist in remote words after clearing deleted words
const remote = remoteWords.find(remoteWord => remoteWord.wordId === word.wordId);
return typeof remote === 'undefined';
});
const localDeletedWords = getLocalDeletedWords();
if (localDeletedWords.length > 0) {
const deletedWordIds = [];
remoteWords = remoteWords.filter(remoteWord => {
const deleted = deletedWords.find(deletedWord => deletedWord.id === remoteWord.wordId);
if (deleted) {
if (deleted.deletedOn > remoteWord.createdOn) {
deletedWordIds.push(deleted.id);
return false;
}
}
return true;
});
let deletePromise;
if (deletedWordIds.length > 0) {
deletePromise = deleteWords(deletedWordIds);
} else {
deletePromise = Promise.resolve(true);
}
deletePromise.then(success => {
if (success) {
clearLocalDeletedWords();
}
});
}
remoteWords.forEach(remoteWord => {
const localWordIndex = words.findIndex(word => word.wordId === remoteWord.wordId);
const localWord = words[localWordIndex];
if (localWord) {
if (localWord.lastUpdated < remoteWord.lastUpdated) {
words[localWordIndex] = remoteWord;
} else if (localWord.lastUpdated > remoteWord.lastUpdated) {
// Add more-recently-updated words to upload
localWordsToUpload.push(localWord);
}
} else {
// If word not found, add it to words
words.push(remoteWord);
}
});
window.currentDictionary.words = words;
sortWords();
saveDictionary(false);
if (localWordsToUpload.length > 0) {
return uploadWords(words);
}
addMessage('Words synchronized');
return Promise.resolve(true);
}
export function uploadWords(words) {
return request({
action: 'set-dictionary-words',
words,
}, successful => {
addMessage('Saved Words to Server');
return successful;
}, error => {
console.error(error);
addMessage('Could not upload words', 10000, 'error');
return false;
});
}
export function deleteWords(wordIds) {
return request({
action: 'delete-words',
wordIds,
}, successful => {
addMessage('Deleted from Server');
return successful;
}, error => {
console.error(error);
addMessage('Could not delete words', 10000, 'error');
saveDeletedWordsLocally(wordIds);
return false;
});
}

View File

@ -0,0 +1,69 @@
import { setCookie } from "../StackOverflow/cookie";
import { DELETED_WORDS_LOCALSTORAGE_KEY } from "./constants";
import { getTimestampInSeconds, cloneObject } from "../../helpers";
import { DEFAULT_DICTIONARY } from "../../constants";
export function saveToken(token) {
setCookie('token', token, 30);
}
export function dictionaryIsDefault() {
const defaultDictionary = cloneObject(DEFAULT_DICTIONARY);
delete defaultDictionary.settings.theme;
delete defaultDictionary.lastUpdated;
delete defaultDictionary.createdOn;
delete defaultDictionary.version;
const currentDictionary = cloneObject(window.currentDictionary);
delete currentDictionary.settings.theme;
delete currentDictionary.lastUpdated;
delete currentDictionary.createdOn;
delete currentDictionary.version;
return JSON.stringify(defaultDictionary) === JSON.stringify(currentDictionary);
}
export function getPublicLink() {
const { externalID } = window.currentDictionary;
let path;
if (externalID) {
path = window.location.pathname.match(new RegExp(externalID + '$'))
? window.location.pathname
: (window.location.pathname.indexOf(externalID) > -1
? window.location.pathname.substring(0, window.location.pathname.indexOf(externalID)) + externalID
: window.location.pathname + externalID
);
} else {
path = '';
}
return 'https://' + document.domain + path;
}
export function saveDeletedWordsLocally(wordIds) {
let storedDeletedWords = getLocalDeletedWords();
wordIds.forEach(wordId => {
if (storedDeletedWords.findIndex(stored => stored.id === wordId) < 0) {
storedDeletedWords.push({
id: wordId,
deletedOn: getTimestampInSeconds(),
});
}
});
window.localStorage.setItem(DELETED_WORDS_LOCALSTORAGE_KEY, JSON.stringify(storedDeletedWords));
}
export function saveDeletedWordLocally(wordId) {
saveDeletedWordsLocally([wordId]);
}
export function getLocalDeletedWords() {
let storedDeletedWords = window.localStorage.getItem(DELETED_WORDS_LOCALSTORAGE_KEY);
if (!storedDeletedWords) {
storedDeletedWords = [];
} else {
storedDeletedWords = JSON.parse(storedDeletedWords);
}
return storedDeletedWords;
}
export function clearLocalDeletedWords() {
window.localStorage.removeItem(DELETED_WORDS_LOCALSTORAGE_KEY);
}

20
src/js/announcements.js Normal file
View File

@ -0,0 +1,20 @@
import { fadeOutElement } from "./utilities";
import { setCookie, getCookie } from "./StackOverflow/cookie";
export function isDismissed(announcementId) {
let dismissed = getCookie(announcementId);
return dismissed === 'dismissed';
}
export function dismiss(announcement) {
if (announcement.id) {
const expireDate = announcement.dataset.expires;
const now = new Date();
const expire = new Date(expireDate);
const timeDiff = Math.abs(now.getTime() - expire.getTime());
const dayDifference = Math.ceil(timeDiff / (1000 * 3600 * 24));
setCookie(announcement.id, 'dismissed', dayDifference + 1);
}
fadeOutElement(announcement)
}

View File

@ -0,0 +1,354 @@
import papa from 'papaparse';
import { renderDictionaryDetails, renderPartsOfSpeech } from "./render/details";
import { renderAll, renderTheme, renderCustomCSS } from "./render";
import { removeTags, cloneObject, getTimestampInSeconds, download, slugify } from "../helpers";
import { LOCAL_STORAGE_KEY, DEFAULT_DICTIONARY } from "../constants";
import { addMessage, getNextId, hasToken, objectValuesAreDifferent } from "./utilities";
import { addWord, sortWords } from "./wordManagement";
import { migrateDictionary } from './migration';
export function updateDictionary () {
renderDictionaryDetails();
}
export function openEditModal() {
const { name, specification, description, partsOfSpeech, alphabeticalOrder } = window.currentDictionary;
const { phonology, phonotactics, orthography, grammar } = window.currentDictionary.details;
const { consonants, vowels, blends } = phonology;
const { allowDuplicates, caseSensitive, sortByDefinition, theme, customCSS, isPublic } = window.currentDictionary.settings;
document.getElementById('editName').value = name;
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(' ');
document.getElementById('editBlends').value = blends.join(' ');
document.getElementById('editPhonologyNotes').value = phonology.notes;
document.getElementById('editOnset').value = phonotactics.onset.join(',');
document.getElementById('editNucleus').value = phonotactics.nucleus.join(',');
document.getElementById('editCoda').value = phonotactics.coda.join(',');
document.getElementById('editPhonotacticsNotes').value = phonotactics.notes;
document.getElementById('editTranslations').value = orthography.translations.join('\n');
document.getElementById('editOrthography').value = orthography.notes;
document.getElementById('editGrammar').value = grammar.notes;
document.getElementById('editPreventDuplicates').checked = !allowDuplicates;
document.getElementById('editCaseSensitive').checked = caseSensitive;
if (allowDuplicates) document.getElementById('editCaseSensitive').disabled = true;
document.getElementById('editSortByDefinition').checked = sortByDefinition;
document.getElementById('editTheme').value = theme;
document.getElementById('editCustomCSS').value = customCSS;
if (hasToken()) {
document.getElementById('editIsPublic').checked = isPublic;
}
document.getElementById('editModal').style.display = '';
Array.from(document.querySelectorAll('#editModal .modal-content section')).forEach(section => section.scrollTop = 0);
}
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 !== '');
updatedDictionary.details.phonology.consonants = document.getElementById('editConsonants').value.split(' ').map(val => val.trim()).filter(val => val !== '');
updatedDictionary.details.phonology.vowels = document.getElementById('editVowels').value.split(' ').map(val => val.trim()).filter(val => val !== '');
updatedDictionary.details.phonology.blends = document.getElementById('editBlends').value.split(' ').map(val => val.trim()).filter(val => val !== '');
updatedDictionary.details.phonology.notes = removeTags(document.getElementById('editPhonologyNotes').value.trim());
updatedDictionary.details.phonotactics.onset = document.getElementById('editOnset').value.split(',').map(val => val.trim()).filter(val => val !== '');
updatedDictionary.details.phonotactics.nucleus = document.getElementById('editNucleus').value.split(',').map(val => val.trim()).filter(val => val !== '');
updatedDictionary.details.phonotactics.coda = document.getElementById('editCoda').value.split(',').map(val => val.trim()).filter(val => val !== '');
updatedDictionary.details.phonotactics.notes = removeTags(document.getElementById('editPhonotacticsNotes').value.trim());
updatedDictionary.details.orthography.translations = document.getElementById('editTranslations').value.split('\n').map(val => val.trim()).filter(val => val !== '');
updatedDictionary.details.orthography.notes = removeTags(document.getElementById('editOrthography').value.trim());
updatedDictionary.details.grammar.notes = removeTags(document.getElementById('editGrammar').value.trim());
updatedDictionary.settings.allowDuplicates = !document.getElementById('editPreventDuplicates').checked;
updatedDictionary.settings.caseSensitive = document.getElementById('editCaseSensitive').checked;
updatedDictionary.settings.sortByDefinition = document.getElementById('editSortByDefinition').checked;
updatedDictionary.settings.theme = document.getElementById('editTheme').value;
updatedDictionary.settings.customCSS = removeTags(document.getElementById('editCustomCSS').value.trim());
if (hasToken()) {
updatedDictionary.settings.isPublic = document.getElementById('editIsPublic').checked;
} else {
updatedDictionary.settings.isPublic = false;
}
if (objectValuesAreDifferent(updatedDictionary, window.currentDictionary)) {
window.currentDictionary = Object.assign(window.currentDictionary, updatedDictionary);
renderTheme();
renderCustomCSS();
renderDictionaryDetails();
renderPartsOfSpeech();
sortWords(true);
addMessage('Saved ' + window.currentDictionary.specification + ' Successfully');
saveDictionary();
if (hasToken()) {
import('./account/index.js').then(account => {
account.uploadDetailsDirect();
account.updateChangeDictionaryOption();
})
}
} else {
addMessage('No changes made to Dictionary Settings.');
}
}
export function saveAndCloseEditModal() {
saveEditModal();
document.getElementById('editModal').style.display = 'none';
}
export function saveDictionary(triggerLastUpdated = true) {
if (triggerLastUpdated) {
window.currentDictionary.lastUpdated = getTimestampInSeconds();
}
window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(window.currentDictionary));
}
export function loadDictionary() {
const storedDictionary = window.localStorage.getItem(LOCAL_STORAGE_KEY);
if (storedDictionary) {
window.currentDictionary = JSON.parse(storedDictionary);
migrateDictionary();
} else {
clearDictionary();
}
}
export function clearDictionary() {
window.currentDictionary = cloneObject(DEFAULT_DICTIONARY);
window.currentDictionary.settings.theme = window.settings.defaultTheme;
}
export function deleteDictionary() {
const deletedId = window.currentDictionary.externalID;
clearDictionary();
saveDictionary();
addMessage('Dictionary Deleted!');
renderAll();
if (hasToken()) {
import('./account/index.js').then(account => {
account.deleteCurrentDictionary(deletedId);
});
}
}
export function confirmDeleteDictionary() {
if (confirm(`Are you sure you want to delete your ${window.currentDictionary.name} ${window.currentDictionary.specification}?\n\nThis cannot be undone!`)) {
const input = prompt(`If you really want to delete your ${window.currentDictionary.name} ${window.currentDictionary.specification} please type DELETE in the text box.\n\nAfter you confirm, cour dicitonary will be PERMANENTLY AND IRRETRIEVABLY DESTROYED!`);
if (input === 'DELETE') {
deleteDictionary();
document.getElementById('editModal').style.display = 'none';
} else {
alert('Your dictionary was NOT deleted');
}
}
}
export function importDictionary() {
const importDictionaryField = document.getElementById('importDictionaryFile');
if (importDictionaryField.files.length === 1) {
if (confirm('Importing a dicitonary file will overwrite and replace your current dictionary!\nDo you want to continue?')) {
addMessage('Importing Dictionary...');
const fileReader = new FileReader();
fileReader.onload = function (fileLoadedEvent) {
const textFromFileLoaded = fileLoadedEvent.target.result;
const importedDictionary = JSON.parse(textFromFileLoaded);
if (importedDictionary && importedDictionary.hasOwnProperty('words')) {
const timestamp = getTimestampInSeconds();
if (!importedDictionary.hasOwnProperty('createdOn')) {
importedDictionary.createdOn = timestamp;
}
if (importedDictionary.words.some(word => !word.hasOwnProperty('createdOn'))) {
importedDictionary.words.forEach(word => {
if (!word.hasOwnProperty('createdOn')) {
word.createdOn = timestamp;
}
if (!word.hasOwnProperty('lastUpdated')) {
word.lastUpdated = timestamp;
}
});
}
if (importedDictionary.hasOwnProperty('externalID')) {
delete importedDictionary.externalID;
}
window.currentDictionary = importedDictionary;
saveDictionary();
renderAll();
importDictionaryField.value = '';
document.getElementById('editModal').style.display = 'none';
addMessage('Dictionary Imported Successfully');
if (hasToken()) {
import('./account/index.js').then(account => {
account.syncImportedDictionary();
});
}
} else {
addMessage('Dictionary could not be imported', 10000, 'error');
}
};
fileReader.readAsText(importDictionaryField.files[0], "UTF-8");
}
}
}
export function importWords() {
const importWordsField = document.getElementById('importWordsCSV');
if (importWordsField.files.length === 1) {
if (confirm('Importing a CSV file with words will add all of the words in the file to your dictionary regardless of duplication!\nDo you want to continue?')) {
addMessage('Importing words...');
const importedWords = [];
papa.parse(importWordsField.files[0], {
header: true,
encoding: "utf-8",
step: results => {
if (results.errors.length > 0) {
results.errors.forEach(err => {
addMessage('Error Importing Word: ' + err, undefined, 'error');
console.error('Error Importing Word: ', err)
});
} else {
const row = results.data;
const wordToImport = {
name: removeTags(row.word).trim(),
pronunciation: removeTags(row.pronunciation).trim(),
partOfSpeech: removeTags(row['part of speech']).trim(),
definition: removeTags(row.definition).trim(),
details: removeTags(row.explanation).trim(),
wordId: getNextId(),
};
if (typeof row['etymology'] !== 'undefined') {
const etymology = removeTags(row['etymology']).trim().split(',').filter(etymology => etymology.trim() !== '');
if (etymology.length > 0) {
wordToImport.etymology = etymology;
}
}
if (typeof row['etymology (comma-separated)'] !== 'undefined') {
const etymology = removeTags(row['etymology (comma-separated)']).trim().split(',').filter(etymology => etymology.trim() !== '');
if (etymology.length > 0) {
wordToImport.etymology = etymology;
}
}
if (typeof row['related words'] !== 'undefined') {
const related = removeTags(row['related words']).trim().split(',').filter(related => related.trim() !== '');
if (related.length > 0) {
wordToImport.related = related;
}
}
if (typeof row['related words (comma-separated)'] !== 'undefined') {
const related = removeTags(row['related words (comma-separated)']).trim().split(',').filter(related => related.trim() !== '');
if (related.length > 0) {
wordToImport.related = related;
}
}
if (typeof row['principal parts'] !== 'undefined') {
const principalParts = removeTags(row['principal parts']).trim().split(',').filter(principalParts => principalParts.trim() !== '');
if (principalParts.length > 0) {
wordToImport.principalParts = principalParts;
}
}
if (typeof row['principal parts (comma-separated)'] !== 'undefined') {
const principalParts = removeTags(row['principal parts (comma-separated)']).trim().split(',').filter(principalParts => principalParts.trim() !== '');
if (principalParts.length > 0) {
wordToImport.principalParts = principalParts;
}
}
const importedWord = addWord(wordToImport, false);
importedWords.push(importedWord);
// Sort and save every 500 words, just in case something goes wrong on large imports
if (importedWords.length % 500 == 499) {
sortWords(false);
}
}
},
complete: () => {
sortWords(false);
renderAll();
importWordsField.value = '';
document.getElementById('editModal').style.display = 'none';
addMessage(`Done Importing ${importedWords.length} Words`);
if (hasToken()) {
import('./account/index.js').then(account => {
account.syncImportedWords(importedWords);
});
}
},
error: err => {
addMessage('Error Importing Words: ' + err, undefined, 'error');
console.error('Error Importing Words: ', err);
},
skipEmptyLines: true,
});
}
}
}
export function exportDictionary() {
addMessage('Exporting JSON...');
setTimeout(() => {
const file = JSON.stringify(window.currentDictionary),
{ name, specification } = window.currentDictionary;
const fileName = slugify(name + '_' + specification) + '.json';
download(file, fileName, 'application/json;charset=utf-8');
}, 1);
}
export function exportWords() {
addMessage('Exporting Words...');
setTimeout(() => {
const { name, specification } = window.currentDictionary;
const fileName = slugify(name + '_' + specification) + '_words.csv';
const words = window.currentDictionary.words.map(word => {
return {
word: word.name,
pronunciation: word.pronunciation,
'part of speech': word.partOfSpeech,
definition: word.definition,
explanation: word.details,
'etymology (comma-separated)': typeof word.etymology !== 'undefined' ? word.etymology.join(',') : '',
'related words (comma-separated)': typeof word.related !== 'undefined' ? word.related.join(',') : '',
'principal parts (comma-separated)': typeof word.principalParts !== 'undefined' ? word.principalParts.join(',') : '',
}
});
const csv = papa.unparse(words, { quotes: true });
download(csv, fileName, 'text/csv;charset=utf-8');
}, 1);
}

36
src/js/displayToggles.js Normal file
View File

@ -0,0 +1,36 @@
import { renderDescription, renderDetails, renderStats } from './render/details';
export function showSection(sectionName) {
switch (sectionName) {
case 'description': showDescription(); break;
case 'details': showDetails(); break;
case 'stats': showStats(); break;
}
}
export function hideDetailsPanel() {
document.getElementById('detailsPanel').style.display = 'none';
}
export function getIsDetailsPanelDisplayed() {
return document.getElementById('detailsPanel').style.display !== 'none';
}
function showDescription() {
const detailsPanel = document.getElementById('detailsPanel');
detailsPanel.style.display = 'block';
renderDescription();
}
function showDetails() {
const detailsPanel = document.getElementById('detailsPanel');
detailsPanel.style.display = 'block';
renderDetails();
}
function showStats() {
const detailsPanel = document.getElementById('detailsPanel');
detailsPanel.style.display = 'block';
renderStats();
}

118
src/js/hotkeys.js Normal file
View File

@ -0,0 +1,118 @@
import { confirmEditWord, submitWordForm } from "./wordManagement";
import { showSection, hideDetailsPanel } from "./displayToggles";
import { renderInfoModal, renderMaximizedTextbox } from "./render/modals";
import { showSearchModal, clearSearchText } from "./search";
import { saveAndCloseSettingsModal, openSettingsModal, saveSettings } from "./settings";
import { saveAndCloseEditModal, openEditModal } from "./dictionaryManagement";
import { addMessage, hideAllModals } from "./utilities";
import helpFile from '../markdown/help.md';
export function enableHotKeys() {
document.addEventListener('keydown', hotKeyActions);
}
export function disableHotKeys() {
document.removeEventListener('keydown', hotKeyActions);
}
export function hotKeyActions(event) {
if (typeof event.key === 'undefined' || typeof event.ctrlKey === 'undefined' || typeof event.altKey === 'undefined') {
addMessage('Hotkeys disabled', undefined, 'error');
console.warn('Browser does not have required event properties for hotkeys.');
window.settings.useHotkeys = false;
saveSettings();
disableHotKeys();
return false;
}
switch (event.key) {
case 'Escape': hideAllModals(); break;
case 'Return':
case 'Enter': {
if (event.ctrlKey) {
if (document.getElementById('settingsModal').style.display !== 'none') {
saveAndCloseSettingsModal();
} else if (document.getElementById('editModal').style.display !== 'none') {
saveAndCloseEditModal();
} else {
submitWord();
}
} break;
}
case 'd': if (event.ctrlKey) {event.preventDefault(); toggleDetailsDisplay();} break;
case 'e': if (event.ctrlKey) {event.preventDefault(); hideAllModals(); openEditModal();} break;
case 'h': if (event.ctrlKey) {event.preventDefault(); hideAllModals(); showHelpModal();} break;
case 'm': if (event.ctrlKey) {event.preventDefault(); maximizeTextarea();} break;
case 's': {
if (event.ctrlKey) {
event.preventDefault();
hideAllModals();
if (event.shiftKey) { // This is a failsafe in case the 'S' case below doesn't work for certain browsers
openSettingsModal();
} else {
showSearchModal();
}
}
break;
}
case 'S': if (event.ctrlKey) {event.preventDefault(); hideAllModals(); openSettingsModal();} break;
case 'Delete':
case 'Backspace': if (event.ctrlKey) {event.preventDefault(); clearSearchText();} break;
}
}
function toggleDetailsDisplay() {
const activeTab = document.querySelector('#detailsSection nav li.active');
Array.from(document.querySelectorAll('#detailsSection nav li')).forEach(li => li.classList.remove('active'));
if (activeTab) {
switch(activeTab.innerText.trim().toLowerCase()) {
case 'description': {
document.querySelector('#detailsSection nav li:nth-child(2)').classList.add('active');
showSection('details');
break;
}
case 'details': {
document.querySelector('#detailsSection nav li:nth-child(3)').classList.add('active');
showSection('stats');
break;
}
case 'stats': {
hideDetailsPanel();
break;
}
}
} else {
document.querySelector('#detailsSection nav li:nth-child(1)').classList.add('active');
showSection('description');
}
}
function submitWord() {
const focused = document.activeElement;
if (focused && focused.id) {
const isSubmittableField = focused.id.includes('wordName') || focused.id.includes('wordDefinition') || focused.id.includes('wordDetails');
if (isSubmittableField) {
if (focused.parentElement.parentElement.classList.contains('edit-form')) {
const wordId = parseInt(focused.parentElement.parentElement.id.replace('editForm_', ''));
confirmEditWord(wordId);
} else {
submitWordForm();
}
}
}
}
function showHelpModal() {
renderInfoModal(helpFile);
}
function maximizeTextarea() {
const focused = document.activeElement;
if (focused) {
const maximizeButton = focused.parentElement.querySelector('.maximize-button');
if (maximizeButton) {
renderMaximizedTextbox(maximizeButton);
}
}
}

128
src/js/migration.js Normal file
View File

@ -0,0 +1,128 @@
import { LOCAL_STORAGE_KEY, DEFAULT_DICTIONARY, MIGRATE_VERSION } from "../constants";
import { saveDictionary } from "./dictionaryManagement";
import { getTimestampInSeconds } from "../helpers";
export default function migrate() {
if (window.location.pathname === '/') {
if (isNotSecure()) {
sendDictionaryToHTTPS();
} else {
checkForReceived();
}
}
}
function isNotSecure() {
return window.location.host !== 'localhost' && window.location.protocol !== 'https:';
}
function sendDictionaryToHTTPS() {
const storedDictionary = window.localStorage.getItem(LOCAL_STORAGE_KEY);
const httpsURL = 'https://' + window.location.host;
if (storedDictionary) {
if (!dictionaryIsOldDefault(storedDictionary)) {
const form = document.createElement('form');
form.action = httpsURL;
form.method = 'POST';
form.hidden = true;
const field = document.createElement('input');
field.name = 'oldDictionaryFromHTTP';
field.value = storedDictionary;
form.appendChild(field);
const blackoutShield = document.createElement('div');
blackoutShield.classList.add('modal-background');
document.body.appendChild(form);
document.body.appendChild(blackoutShield);
alert('You are about to be redirected to the secure https version of Lexiconga. Please update your bookmarks.')
form.submit();
return;
}
}
window.location = httpsURL;
}
function dictionaryIsOldDefault(dictionaryJSON) {
const defaultDictionary = {
name: "New",
description: "A new dictionary.",
// createdBy: publicName,
words: [],
nextWordId: 1,
settings: {
allowDuplicates: false,
caseSensitive: false,
partsOfSpeech: "Noun,Adjective,Verb,Adverb,Preposition,Pronoun,Conjunction",
sortByEquivalent: false,
isComplete: false,
isPublic: false
},
externalID: 0
}
const dictionary = JSON.parse(dictionaryJSON);
delete dictionary.createdBy;
return JSON.stringify(defaultDictionary) === JSON.stringify(dictionary);
}
function checkForReceived() {
if (window.hasOwnProperty('dictionaryImportedFromHTTP')) {
let saveOld = true;
const storedDictionary = window.localStorage.getItem(LOCAL_STORAGE_KEY);
if (storedDictionary) {
saveOld = confirm('You have an old local dictionary to import. Would you like to overwrite your current local dictionary?');
}
if (saveOld) {
window.localStorage.setItem(LOCAL_STORAGE_KEY, window.dictionaryImportedFromHTTP);
delete window.dictionaryImportedFromHTTP;
}
}
}
export function migrateDictionary() {
let migrated = false;
if (!window.currentDictionary.hasOwnProperty('version')) {
const fixStupidOldNonsense = string => string.replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&#92;/g, '\\').replace(/<br>/g, '\n');
window.currentDictionary.description = fixStupidOldNonsense(window.currentDictionary.description);
const timestamp = getTimestampInSeconds();
window.currentDictionary.words = window.currentDictionary.words.map(word => {
word.definition = word.simpleDefinition;
delete word.simpleDefinition;
word.details = fixStupidOldNonsense(word.longDefinition);
delete word.longDefinition;
word.lastUpdated = timestamp;
word.createdOn = timestamp;
return word;
});
window.currentDictionary = Object.assign({}, DEFAULT_DICTIONARY, window.currentDictionary);
window.currentDictionary.partsOfSpeech = window.currentDictionary.settings.partsOfSpeech.split(',').map(val => val.trim()).filter(val => val !== '');
delete window.currentDictionary.settings.partsOfSpeech;
delete window.currentDictionary.nextWordId;
window.currentDictionary.settings.sortByDefinition = window.currentDictionary.settings.sortByEquivalent;
delete window.currentDictionary.settings.sortByEquivalent;
window.currentDictionary.settings.theme = 'default';
delete window.currentDictionary.settings.isComplete;
migrated = true;
} else if (window.currentDictionary.version !== MIGRATE_VERSION) {
switch (window.currentDictionary.version) {
default: console.error('Unknown version'); break;
case '2.0.0': {
window.currentDictionary.details.phonology.notes = '';
window.currentDictionary.details.phonotactics = Object.assign({}, window.currentDictionary.details.phonology.phonotactics);
delete window.currentDictionary.details.phonology.phonotactics;
window.currentDictionary.details.phonotactics.notes = window.currentDictionary.details.phonotactics.exceptions;
delete window.currentDictionary.details.phonotactics.exceptions;
window.currentDictionary.details.orthography.translations = [];
window.currentDictionary.settings.customCSS = '';
window.currentDictionary = Object.assign({}, DEFAULT_DICTIONARY, window.currentDictionary);
window.currentDictionary.version = MIGRATE_VERSION;
migrated = true;
// break; By skipping the break, all migrations can happen in sequence.
}
}
}
if (migrated) {
saveDictionary();
}
}

36
src/js/pagination.js Normal file
View File

@ -0,0 +1,36 @@
import { DEFAULT_PAGE_SIZE } from '../constants';
import { renderWords } from "./render/words";
export function getPaginationData(words) {
const numWords = words.length;
const pageSize = window.localStorage.getItem('pageSize') ? parseInt(window.localStorage.getItem('pageSize')) : DEFAULT_PAGE_SIZE;
const pages = Math.floor(numWords / pageSize);
const currentPage = window.hasOwnProperty('currentPage') ? window.currentPage : 0;
const pageStart = currentPage * pageSize;
const pageEnd = typeof words[pageStart + pageSize] !== 'undefined'
? pageStart + pageSize : words.length - 1;
return { numWords, pageSize, pages, currentPage, pageStart, pageEnd, };
}
export function goToPage(page) {
if (typeof page.target !== 'undefined') {
page = page.target.value;
}
window.currentPage = parseFloat(page);
Array.from(document.getElementsByClassName('pagination')).forEach(pagination => {
pagination.innerHTML = `<span class="loader">Loading Page ${window.currentPage + 1}...</span>`;
});
setTimeout(renderWords, 1);
// renderWords();
}
export function goToNextPage() {
goToPage((window.hasOwnProperty('currentPage') ? window.currentPage : 0) + 1);
}
export function goToPreviousPage() {
goToPage((window.hasOwnProperty('currentPage') ? window.currentPage : 1) - 1);
}

141
src/js/render/details.js Normal file
View File

@ -0,0 +1,141 @@
import md from 'marked';
import { removeTags, slugify } from '../../helpers';
import { getWordsStats, hasToken } from '../utilities';
import { showSection } from '../displayToggles';
import { setupSearchFilters } from '../setupListeners/search';
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 = '&#10150;';
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 = '<div class="content">' + descriptionHTML + '</div>';
}
export function renderDetails() {
const { partsOfSpeech, alphabeticalOrder } = window.currentDictionary;
const { phonology, phonotactics, orthography, grammar } = window.currentDictionary.details;
const partsOfSpeechHTML = `<p><strong>Parts of Speech</strong><br>${partsOfSpeech.map(partOfSpeech => '<span class="tag">' + partOfSpeech + '</span>').join(' ')}</div>`;
const alphabeticalOrderHTML = `<p><strong>Alphabetical Order</strong><br>${
(alphabeticalOrder.length > 0 ? alphabeticalOrder : ['English Alphabet']).map(letter => `<span class="tag">${letter}</span>`).join(' ')
}</div>`;
const generalHTML = `<h2>General</h2>${partsOfSpeechHTML}${alphabeticalOrderHTML}`;
const { consonants, vowels, blends } = phonology
const consonantHTML = `<p><strong>Consonants</strong><br>${consonants.map(letter => `<span class="tag">${letter}</span>`).join(' ')}</p>`;
const vowelHTML = `<p><strong>Vowels</strong><br>${vowels.map(letter => `<span class="tag">${letter}</span>`).join(' ')}</p>`;
const blendHTML = blends.length > 0 ? `<p><strong>Polyphthongs&nbsp;/&nbsp;Blends</strong><br>${blends.map(letter => `<span class="tag">${letter}</span>`).join(' ')}</p>` : '';
const phonologyNotesHTML = phonology.notes.trim().length > 0 ? '<p><strong>Notes</strong></p><div>' + md(parseReferences(removeTags(phonology.notes))) + '</div>' : '';
const phonologyHTML = `<h2>Phonology</h2>
<div class="split two">
<div>${consonantHTML}</div>
<div>${vowelHTML}</div>
</div>
${blendHTML}
${phonologyNotesHTML}`;
const { onset, nucleus, coda } = phonotactics;
const onsetHTML = `<p><strong>Onset</strong><br>${onset.map(letter => `<span class="tag">${letter}</span>`).join(' ')}</p>`;
const nucleusHTML = `<p><strong>Nucleus</strong><br>${nucleus.map(letter => `<span class="tag">${letter}</span>`).join(' ')}</p>`;
const codaHTML = `<p><strong>Coda</strong><br>${coda.map(letter => `<span class="tag">${letter}</span>`).join(' ')}</p>`;
const phonotacticsNotesHTML = phonotactics.notes.trim().length > 0 ? '<p><strong>Notes</strong></p><div>' + md(parseReferences(removeTags(phonotactics.notes))) + '</div>' : '';
const phonotacticsHTML = onset.length + nucleus.length + coda.length + phonotacticsNotesHTML.length > 0
? `<h2>Phonotactics</h2>
${onset.length > 0 || nucleus.length > 0 || coda.length > 0
? `<div class="split three">
<div>${onsetHTML}</div>
<div>${nucleusHTML}</div>
<div>${codaHTML}</div>
</div>` : ''}
${phonotacticsNotesHTML}`
: '';
const { translations } = orthography;
const translationsHTML = translations.length > 0 ? `<p><strong>Translations</strong><br>${translations.map(translation => {
translation = translation.split('=').map(value => value.trim());
if (translation.length > 1 && translation[0] !== '' && translation[1] !== '') {
return `<span><span class="tag">${translation[0]}</span><span class="tag orthographic-translation">${translation[1]}</span></span>`;
}
return false;
}).filter(html => html !== false).join(' ')}</p>` : '';
const orthographyNotesHTML = orthography.notes.trim().length > 0 ? '<p><strong>Notes</strong><br>' + md(parseReferences(removeTags(orthography.notes))) + '</div>' : '';
const orthographyHTML = translations.length + orthographyNotesHTML.length > 0
? `<h2>Orthography</h2>
${translationsHTML}
${orthographyNotesHTML}`
: '';
const grammarHTML = grammar.notes.trim().length > 0 ? '<h2>Grammar</h2><div>'
+ (grammar.notes.trim().length > 0 ? md(parseReferences(removeTags(grammar.notes))) : '')
+ '</div>' : '';
detailsPanel.innerHTML = generalHTML + phonologyHTML + phonotacticsHTML + orthographyHTML + grammarHTML;
}
export function renderStats() {
const wordStats = getWordsStats();
const numberOfWordsHTML = `<p><strong>Number of Words</strong><br>${wordStats.numberOfWords.map(stat => `<span><span class="tag">${stat.name}</span><span class="tag">${stat.value}</span></span>`).join(' ')}</p>`;
const wordLengthHTML = `<p><strong>Word Length</strong><br><span><span class="tag">Shortest</span><span class="tag">${wordStats.wordLength.shortest}</span></span>
<span><span class="tag">Longest</span><span class="tag">${wordStats.wordLength.longest}</span></span>
<span><span class="tag">Average</span><span class="tag">${wordStats.wordLength.average}</span></span></p>`;
const letterDistributionHTML = `<p><strong>Letter Distribution</strong><br>${wordStats.letterDistribution.map(stat => `<span title="${stat.number} ${stat.letter}'s total"><span class="tag">${stat.letter}</span><span class="tag">${stat.percentage.toFixed(2)}</span></span>`).join(' ')}</p>`;
const totalLettersHTML = `<p><strong>${wordStats.totalLetters} Total Letters</strong></p>`;
detailsPanel.innerHTML = numberOfWordsHTML + wordLengthHTML + letterDistributionHTML + totalLettersHTML;
}
export function renderPartsOfSpeech(onlyOptions = false) {
let optionsHTML = '<option value=""></option>',
searchHTML = '<label>Unclassified <input type="checkbox" checked id="searchPartOfSpeech__None"></label>';
window.currentDictionary.partsOfSpeech.forEach(partOfSpeech => {
partOfSpeech = removeTags(partOfSpeech);
optionsHTML += `<option value="${partOfSpeech.replace(/"/g, '&quot;')}">${partOfSpeech}</option>`;
searchHTML += `<label>${partOfSpeech} <input type="checkbox" checked id="searchPartOfSpeech_${slugify(partOfSpeech)}"></label>`;
});
searchHTML += `<a class="small button" id="checkAllFilters">Check All</a> <a class="small button" id="uncheckAllFilters">Uncheck All</a>`;
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();
}

31
src/js/render/index.js Normal file
View File

@ -0,0 +1,31 @@
import { renderDictionaryDetails, renderPartsOfSpeech } from './details';
import { renderWords } from './words';
import { renderTemplateSelectOptions } from './settings';
export function renderAll() {
renderTheme();
renderCustomCSS();
renderDictionaryDetails();
renderPartsOfSpeech();
renderTemplateSelectOptions();
renderWords();
}
export function renderTheme() {
const { theme } = window.currentDictionary.settings;
document.body.id = theme + 'Theme';
}
export function renderCustomCSS() {
const { customCSS } = window.currentDictionary.settings;
const stylingId = 'customCSS';
const stylingElement = document.getElementById(stylingId);
if (!stylingElement) {
const styling = document.createElement('style');
styling.id = stylingId;
styling.innerHTML = customCSS;
document.body.appendChild(styling);
} else {
stylingElement.innerHTML = customCSS;
}
}

74
src/js/render/modals.js Normal file
View File

@ -0,0 +1,74 @@
import { setupIPAFields } from '../setupListeners';
import {
setupMaximizeModal,
setupInfoModal,
setupIPATable,
} from '../setupListeners/modals';
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 = `<div class="modal-background"></div>
<div class="modal-content">
<a class="close-button">&times;&#xFE0E;</a>
<header><label>${label} <input value="${textBox.value}" class="ipa-field"></label></header>
<section>
${ipaTableFile}
</section>
<footer><a class="button done-button">Done</a></footer>
</div>`;
document.body.appendChild(modalElement);
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 = `<div class="modal-background"></div>
<div class="modal-content">
<a class="close-button">&times;&#xFE0E;</a>
<header><h3>${label}</h3></header>
<section>
<textarea>${textBox.value}</textarea>
</section>
<footer><a class="button done-button">Done</a></footer>
</div>`;
document.body.appendChild(modalElement);
setupMaximizeModal(modalElement, textBox);
}
export function renderInfoModal(content) {
const modalElement = document.createElement('section');
modalElement.classList.add('modal', 'info-modal');
modalElement.innerHTML = `<div class="modal-background"></div>
<div class="modal-content">
<a class="close-button">&times;&#xFE0E;</a>
<section class="info-modal">
<div class="content">
${content}
</div>
</section>
</div>`;
document.body.appendChild(modalElement);
setupInfoModal(modalElement);
}

46
src/js/render/settings.js Normal file
View File

@ -0,0 +1,46 @@
import { setupTemplateSelectOptions } from "../setupListeners/settings";
export function renderTemplateSelectOptions() {
const { templates } = window.settings;
if (typeof templates !== 'undefined') {
const templatesOptionsHTML = templates.map((template, index) => {
return `<option value="${index.toString()}">${template.name}</options>`;
}).join('');
Array.from(document.getElementsByClassName('template-select')).forEach(select => {
if (select.id !== 'savedDetailsTemplates' && templates.length < 1) {
return select.parentElement.style.display = 'none';
} else {
select.parentElement.style.display = '';
}
select.innerHTML = '<option value="" selected="selected">None Selected</option>' + templatesOptionsHTML;
});
setupTemplateSelectOptions();
}
}
export function showTemplateEditor(show = true) {
document.getElementById('templateFields').style.display = show ? '' : 'none';
if (show) {
document.getElementById('templateTextarea').focus();
document.querySelector('#settingsModal .modal-content section').scrollTop = 9999;
} else {
clearTemplateEditor();
}
}
export function showSelectedTemplate(template, index) {
const nameField = document.getElementById('templateNameField');
nameField.value = template.name;
nameField.setAttribute('template', index.toString());
document.getElementById('templateTextarea').value = template.template;
showTemplateEditor(true);
}
export function clearTemplateEditor() {
document.getElementById('savedDetailsTemplates').value = '';
document.getElementById('templateNameField').value = '';
document.getElementById('templateTextarea').value = '';
}

213
src/js/render/words.js Normal file
View File

@ -0,0 +1,213 @@
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/words';
// import { getPaginationData } from '../pagination';
import { getOpenEditForms, translateOrthography, parseReferences, getWordReferenceMarkdown } from '../wordManagement';
import { getPublicLink } from '../account/utilities';
import { renderPartsOfSpeech } from './details';
import { renderTemplateSelectOptions } from './settings';
export function renderWord(savedWord, isPublic) {
const word = highlightSearchTerm({
name: removeTags(savedWord.name),
pronunciation: removeTags(savedWord.pronunciation),
partOfSpeech: removeTags(savedWord.partOfSpeech),
definition: removeTags(savedWord.definition),
details: parseReferences(removeTags(savedWord.details)),
etymology: typeof savedWord.etymology === 'undefined' || savedWord.etymology.length < 1 ? null
: savedWord.etymology.map(root => getWordReferenceMarkdown(removeTags(root))).join(', '),
related: typeof savedWord.related === 'undefined' || savedWord.related.length < 1 ? null
: savedWord.related.map(relatedWord => getWordReferenceMarkdown(removeTags(relatedWord))).join(', '),
principalParts: typeof savedWord.principalParts === 'undefined' || savedWord.principalParts.length < 1 ? null
: savedWord.principalParts.join(', '),
wordId: savedWord.wordId,
});
const homonymnNumber = getHomonymnNumber(savedWord);
const shareLink = window.currentDictionary.hasOwnProperty('externalID') ? getPublicLink() + '/' + word.wordId : '';
let wordNameDisplay = translateOrthography(word.name);
return `<article class="entry" id="${word.wordId}">
<header>
<h4 class="word"><span class="orthographic-translation">${wordNameDisplay}</span>${homonymnNumber > 0 ? ' <sub>' + homonymnNumber.toString() + '</sub>' : ''}</h4>
${word.principalParts === null ? '' : `<span class="principalParts">(${word.principalParts})</span>`}
<span class="pronunciation">${word.pronunciation}</span>
<span class="part-of-speech">${word.partOfSpeech}</span>
${isPublic ? `<a class="small button share-link" href="${shareLink}" target="_blank" title="Public Link to Word">&#10150;</a>` : ''}
<span class="small button word-option-button">Options</span>
<div class="word-option-list" style="display:none;">
<div class="word-option" id="edit_${word.wordId}">Edit</div>
<div class="word-option" id="delete_${word.wordId}">Delete</div>
</div>
</header>
<dl>
<dt class="definition">${word.definition}</dt>
<dd class="details">
${md(word.details)}
</dd>
${word.etymology === null && word.related === null ? '' : `<hr>`}
${word.etymology === null ? '' : `<dt>Etymology <small>(Root Word${savedWord.etymology.length !== 1 ? 's' : ''})</small></dt>
<dd class="etymology">
${md(word.etymology).replace(/<\/?p>/g, '')}
</dd>`}
${word.related === null ? '' : `<dt>Related Word${savedWord.related.length !== 1 ? 's' : ''}</dt>
<dd class="related">
${md(word.related).replace(/<\/?p>/g, '')}
</dd>`}
</dl>
</article>`;
}
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 = `<article class="entry">
<header>
<h4 class="word">No Words Created</h4>
</header>
<dl>
<dt class="definition">Use the Word Form to create words or click the Help button below!</dt>
</dl>
</article>`;
} else {
words = getMatchingSearchWords();
if (words.length === 0) {
wordsHTML = `<article class="entry">
<header>
<h4 class="word">No Search Results</h4>
</header>
<dl>
<dt class="definition">Edit your search or filter to show words.</dt>
</dl>
</article>`;
}
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(savedWord => {
words.forEach(savedWord => {
wordsHTML += renderWord(savedWord, isPublic);
});
}
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 ? '<span class="button prev-button">&laquo; Previous</span>' : '')
// + '<select class="page-selector">';
// for (let i = 0; i < paginationData.pages; i++) {
// paginationHTML += `<option value="${i}"${paginationData.currentPage === i ? ' selected' : ''}>Page ${i + 1}</option>`;
// }
// paginationHTML += '</select>'
// + (paginationData.currentPage < paginationData.pages - 1 ? '<span class="button next-button">Next &raquo;</span>' : '');
// 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 escapeQuotes = (value) => value.replace(/"/g, '&quot;');
const wordHasAdvancedFields = (word.hasOwnProperty('etymology') && word.etymology)
|| (word.hasOwnProperty('related') && word.related) || (word.hasOwnProperty('principalParts') && word.principalParts);
const ipaPronunciationField = `<input id="wordPronunciation_${wordId}" class="ipa-field" maxlength="200" value="${escapeQuotes(word.pronunciation)}"><br>
<a class="label-help-button ipa-field-help-button">Field Help</a>`;
const plainPronunciationField = `<input id="wordPronunciation_${wordId}" maxlength="200" value="${escapeQuotes(word.pronunciation)}">`;
const editForm = `<form id="editForm_${wordId}" class="edit-form">
<label>Word<span class="red">*</span><br>
<input id="wordName_${wordId}" maxlength="200" value="${escapeQuotes(word.name)}">
</label>
<label>Pronunciation<a class="label-button ipa-table-button">IPA Chart</a><br>
${window.settings.useIPAPronunciationField ? ipaPronunciationField : plainPronunciationField}
</label>
<label>Part of Speech<br>
<select id="wordPartOfSpeech_${wordId}" class="part-of-speech-select">
<option value="${escapeQuotes(word.partOfSpeech)}" selected>${word.partOfSpeech}</option>
</select>
</label>
<label>Definition<span class="red">*</span><br>
<input id="wordDefinition_${wordId}" maxlength="2500" value="${escapeQuotes(word.definition)}" placeholder="Equivalent words">
</label>
<label>Details<span class="red">*</span><a class="label-button maximize-button">Maximize</a><br>
<textarea id="wordDetails_${wordId}" placeholder="Markdown formatting allowed">${word.details}</textarea>
</label>
<label>
<a id="expandAdvancedForm_${wordId}" class="small button expand-advanced-form">${wordHasAdvancedFields || window.settings.showAdvanced ? 'Hide' : 'Show'} Advanced Fields</a>
</label>
<div id="advancedForm_${wordId}" class="advanced-word-form" style="display:${wordHasAdvancedFields || window.settings.showAdvanced ? 'block' : 'none'};">
<label>Details Field Templates
<select id="templateSelect_${wordId}" class="template-select">
</select>
<small>Choose one to fill the details field. (Note: Will erase anything currently there.)</small>
</label>
<label>Etymology / Root Words<br>
<input id="wordEtymology_${wordId}" maxlength="2500" placeholder="comma,separated,root,words" value="${word.hasOwnProperty('etymology') ? escapeQuotes(word.etymology.toString()) : ''}">
</label>
<label>Related Words<br>
<input id="wordRelated_${wordId}" maxlength="2500" placeholder="comma,separated,related,words" value="${word.hasOwnProperty('related') ? escapeQuotes(word.related.toString()) : ''}">
</label>
<label>Principal Parts<a href="https://en.wikipedia.org/wiki/Principal_parts" target="_blank" class="label-button">What's This?</a><br>
<input id="wordPrincipalParts_${wordId}" maxlength="2500" placeholder="comma,separated,principal,parts" value="${word.hasOwnProperty('principalParts') ? escapeQuotes(word.principalParts.toString()) : ''}">
</label>
</div>
<div id="wordErrorMessage_${wordId}"></div>
<a class="button edit-save-changes" id="editWordButton_${wordId}">Save Changes</a>
<a class="button edit-cancel">Cancel Edit</a>
</form>`;
document.getElementById(wordId.toString()).innerHTML = editForm;
setupWordEditFormButtons();
renderPartsOfSpeech(true);
renderTemplateSelectOptions();
}
}

197
src/js/search.js Normal file
View File

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

212
src/js/settings.js Normal file
View File

@ -0,0 +1,212 @@
import { SETTINGS_KEY, DEFAULT_SETTINGS } from "../constants";
import { cloneObject, removeTags } from "../helpers";
import { usePhondueDigraphs } from "./KeyboardFire/phondue/ipaField";
import { renderWords } from "./render/words";
import { addMessage, hasToken, objectValuesAreDifferent } from "./utilities";
import { enableHotKeys, disableHotKeys } from "./hotkeys";
import { showTemplateEditor, renderTemplateSelectOptions, showSelectedTemplate } from "./render/settings";
export function loadSettings() {
const storedSettings = window.localStorage.getItem(SETTINGS_KEY);
window.settings = storedSettings ? JSON.parse(storedSettings) : cloneObject(DEFAULT_SETTINGS);
toggleIPAPronunciationFields(false);
toggleShowAdvancedFields();
}
export function saveSettings() {
window.localStorage.setItem(SETTINGS_KEY, JSON.stringify(window.settings));
addMessage('Settings Saved!');
}
export function openSettingsModal() {
const { useIPAPronunciationField, useHotkeys, showAdvanced, defaultTheme, templates } = window.settings;
document.getElementById('settingsUseIPA').checked = useIPAPronunciationField;
document.getElementById('settingsUseHotkeys').checked = useHotkeys;
document.getElementById('settingsShowAdvanced').checked = showAdvanced;
document.getElementById('settingsDefaultTheme').value = defaultTheme;
renderTemplateSelectOptions();
showTemplateEditor(false);
document.getElementById('settingsModal').style.display = '';
document.querySelector('#settingsModal .modal-content section').scrollTop = 0;
}
export function updateTemplateSelects() {
const { templates } = window.settings;
if (typeof templates !== 'undefined') {
const templatesOptionsHTML = templates.map((template, index) => {
return `<option value="${index.toString()}">${template.name}</options>`;
}).join('');
document.getElementById('savedDetailsTemplates').innerHTML = templatesOptionsHTML;
const templateSelects = document.getElementsByClassName('template-select');
Array.from(templateSelects).forEach(select => {
select.removeEventListener('click', showWordOptions);
select.innerHTML = templatesOptionsHTML;
select.addEventListener('click', showWordOptions);
})
}
}
export function createTemplate() {
window.settings.templates.push({ name: 'New Blank Template', template: '' });
const newTemplateIndex = window.settings.templates.length - 1;
renderTemplateSelectOptions();
document.getElementById('savedDetailsTemplates').value = (newTemplateIndex).toString();
editSavedTemplate(newTemplateIndex);
}
export function saveTemplate() {
const nameField = document.getElementById('templateNameField');
const name = nameField.value.trim();
const index = parseInt(nameField.getAttribute('template'));
const template = document.getElementById('templateTextarea').value;
window.settings.templates[index] = { name, template };
let storedSettings = window.localStorage.getItem(SETTINGS_KEY);
storedSettings = storedSettings ? JSON.parse(storedSettings) : cloneObject(DEFAULT_SETTINGS);
storedSettings.templates = window.settings.templates;
window.localStorage.setItem(SETTINGS_KEY, JSON.stringify(storedSettings));
addMessage('Templates Saved!');
showTemplateEditor(false);
renderTemplateSelectOptions();
}
export function deleteSelectedTemplate() {
const nameField = document.getElementById('templateNameField');
const index = nameField.getAttribute('template');
const name = window.settings.templates[index].name;
if (confirm(`Are you sure you want to delete the "${name}" template? This cannot be undone!`)) {
delete window.settings.templates[index];
window.settings.templates = window.settings.templates.filter(template => template != null);
saveSettings();
showTemplateEditor(false);
renderTemplateSelectOptions();
}
}
export function editSavedTemplate(selectEvent) {
const { templates } = window.settings;
const selectedIndex = typeof selectEvent.target !== 'undefined' ? selectEvent.target.value : selectEvent;
if (selectedIndex !== '') {
showSelectedTemplate(templates[selectedIndex], selectedIndex);
} else {
showTemplateEditor(false);
}
}
export function saveSettingsModal() {
const updatedSettings = cloneObject(window.settings);
updatedSettings.useIPAPronunciationField = document.getElementById('settingsUseIPA').checked;
updatedSettings.useHotkeys = document.getElementById('settingsUseHotkeys').checked;
updatedSettings.showAdvanced = document.getElementById('settingsShowAdvanced').checked;
updatedSettings.defaultTheme = document.getElementById('settingsDefaultTheme').value;
if (hasToken()) {
import('./account/index.js').then(account => {
const emailField = document.getElementById('accountSettingsEmail');
let email = removeTags(emailField.value).trim();
const publicName = document.getElementById('accountSettingsPublicName');
if (!/.+@.+\..+/.test(email)) {
email = window.account.email;
emailField.value = email;
}
const updatedAccount = cloneObject(window.account);
updatedAccount.email = email;
updatedAccount.publicName = removeTags(publicName.value).trim();
updatedAccount.allowEmails = document.getElementById('accountSettingsAllowEmails').checked;
const newPassword = document.getElementById('accountSettingsNewPassword').value;
if (objectValuesAreDifferent(updatedAccount, window.account)) {
window.account = Object.assign(window.account, updatedAccount);
account.editAccount(Object.assign({ newPassword }, window.account));
} else {
addMessage('No changes made to Account.');
}
});
}
if (objectValuesAreDifferent(updatedSettings, window.settings)) {
window.settings = Object.assign(window.settings, updatedSettings);
saveSettings();
toggleHotkeysEnabled();
toggleIPAPronunciationFields();
toggleShowAdvancedFields();
} else {
addMessage('No changes made to Settings.');
}
}
export function saveAndCloseSettingsModal() {
saveSettingsModal();
document.getElementById('settingsModal').style.display = 'none';
}
export function toggleHotkeysEnabled() {
disableHotKeys();
if (window.settings.useHotkeys) {
enableHotKeys();
}
}
export function toggleIPAPronunciationFields(render = true) {
const ipaButtons = document.querySelectorAll('.ipa-field-help-button'),
ipaFields = document.querySelectorAll('.ipa-field');
if (!window.settings.useIPAPronunciationField) {
Array.from(ipaButtons).forEach(button => {
button.style.display = 'none';
});
Array.from(ipaFields).forEach(field => {
field.removeEventListener('keypress', usePhondueDigraphs);
});
} else {
Array.from(ipaButtons).forEach(button => {
button.style.display = '';
});
Array.from(ipaFields).forEach(field => {
field.addEventListener('keypress', usePhondueDigraphs);
});
}
if (render) {
renderWords();
}
}
export function toggleShowAdvancedFields() {
const buttons = document.getElementsByClassName('expand-advanced-form'),
forms = document.getElementsByClassName('advanced-word-form');
const formsWithFilledFields = [];
Array.from(forms).forEach(form => {
const fields = form.querySelectorAll('input, textarea');
const formHasFieldFilled = Array.from(fields).some(field => field.value.trim() !== '');
if (window.settings.showAdvanced || formHasFieldFilled) {
form.style.display = 'block';
} else {
form.style.display = 'none';
}
if (formHasFieldFilled) {
formsWithFilledFields.push(form.id.replace('advancedForm', ''));
}
});
Array.from(buttons).forEach(button => {
const formHasFilledField = formsWithFilledFields.includes(button.id.replace('expandAdvancedForm', ''));
if (window.settings.showAdvanced || formHasFilledField) {
button.innerText = 'Hide Advanced Fields';
} else {
button.innerText = 'Show Advanced Fields';
}
});
}

View File

@ -0,0 +1,52 @@
import { renderMaximizedTextbox, renderInfoModal, renderIPATable, renderIPAHelp } from '../render/modals';
import helpFile from '../../markdown/help.md';
import termsFile from '../../markdown/terms.md';
import privacyFile from '../../markdown/privacy.md';
import { setupSearchBar } from './search';
import { setupSettingsModal } from './modals';
export function setupHeaderButtons() {
setupSearchBar();
setupSettingsModal();
document.getElementById('loginCreateAccountButton').addEventListener('click', () => {
import('../account/index.js').then(account => {
account.showLoginForm();
});
});
}
export function setupIPAButtons() {
const ipaTableButtons = document.getElementsByClassName('ipa-table-button'),
ipaFieldHelpButtons = document.getElementsByClassName('ipa-field-help-button');
Array.from(ipaTableButtons).forEach(button => {
button.removeEventListener('click', renderIPATable);
button.addEventListener('click', renderIPATable);
});
Array.from(ipaFieldHelpButtons).forEach(button => {
button.removeEventListener('click', renderIPAHelp);
button.addEventListener('click', renderIPAHelp);
});
}
export function setupMaximizeButtons() {
const maximizeButtons = document.getElementsByClassName('maximize-button');
Array.from(maximizeButtons).forEach(button => {
button.removeEventListener('click', renderMaximizedTextbox);
button.addEventListener('click', renderMaximizedTextbox);
});
}
export function setupInfoButtons() {
document.getElementById('helpInfoButton').addEventListener('click', () => {
renderInfoModal(helpFile);
});
document.getElementById('termsInfoButton').addEventListener('click', () => {
renderInfoModal(termsFile);
});
document.getElementById('privacyInfoButton').addEventListener('click', () => {
renderInfoModal(privacyFile);
});
}

View File

@ -0,0 +1,68 @@
import { showSection, hideDetailsPanel } from '../displayToggles';
import { openEditModal, saveEditModal, saveAndCloseEditModal, exportDictionary, exportWords, importDictionary, importWords, confirmDeleteDictionary } from '../dictionaryManagement';
import { setupMaximizeButtons } from './buttons';
export function setupDetailsTabs() {
const tabs = document.querySelectorAll('#detailsSection nav li');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
const section = tab.innerText.toLowerCase();
if (section === 'edit') {
openEditModal();
} else {
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 setupEditFormTabs() {
const tabs = document.querySelectorAll('#editModal nav li');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => {
t.classList.remove('active');
document.getElementById('edit' + t.innerText + 'Tab').style.display = 'none';
});
tab.classList.add('active');
const tabSection = document.getElementById('edit' + tab.innerText + 'Tab');
tabSection.style.display = '';
tabSection.scrollTop = 0;
});
});
}
function setupEditFormInteractions() {
const preventDuplicatesBox = document.getElementById('editPreventDuplicates');
preventDuplicatesBox.addEventListener('change', () => {
const caseSensitiveBox = document.getElementById('editCaseSensitive');
if (preventDuplicatesBox.checked) {
caseSensitiveBox.disabled = false;
} else {
caseSensitiveBox.disabled = true;
caseSensitiveBox.checked = false;
}
});
}
function setupEditFormButtons() {
document.getElementById('editSave').addEventListener('click', saveEditModal);
document.getElementById('editSaveAndClose').addEventListener('click', saveAndCloseEditModal);
document.getElementById('importDictionaryFile').addEventListener('change', importDictionary);
document.getElementById('importWordsCSV').addEventListener('change', importWords);
document.getElementById('exportDictionaryButton').addEventListener('click', exportDictionary);
document.getElementById('exportWordsButton').addEventListener('click', exportWords);
document.getElementById('deleteDictionaryButton').addEventListener('click', confirmDeleteDictionary);
setupMaximizeButtons();
}

View File

@ -0,0 +1,45 @@
import { usePhondueDigraphs } from '../KeyboardFire/phondue/ipaField';
import { enableHotKeys } from '../hotkeys';
import { dismiss, isDismissed } from '../announcements';
import { fadeOutElement } from '../utilities';
import { setupDetailsTabs } from './details';
import { setupWordForm, setupMobileWordFormButton } from './words';
import { setupIPAButtons, setupHeaderButtons, setupInfoButtons } from './buttons';
import { setupTemplateForm, setupTemplateSelectOptions } from './settings';
export default function setupListeners() {
setupAnnouncements();
setupDetailsTabs();
setupTemplateForm();
setupHeaderButtons();
setupWordForm();
setupMobileWordFormButton();
setupInfoButtons();
setupTemplateSelectOptions();
if (window.settings.useHotkeys) {
enableHotKeys();
}
}
function setupAnnouncements() {
const announcements = document.querySelectorAll('.announcement');
Array.from(announcements).forEach(announcement => {
if (announcement.id && isDismissed(announcement.id)) {
fadeOutElement(announcement);
} else {
announcement.querySelector('.close-button').addEventListener('click', () => dismiss(announcement));
}
});
}
export function setupIPAFields() {
if (window.settings.useIPAPronunciationField) {
const ipaFields = document.getElementsByClassName('ipa-field');
Array.from(ipaFields).forEach(field => {
field.removeEventListener('keypress', usePhondueDigraphs);
field.addEventListener('keypress', usePhondueDigraphs);
});
}
setupIPAButtons();
}

View File

@ -0,0 +1,80 @@
import { insertAtCursor, getInputSelection, setSelectionRange } from '../StackOverflow/inputCursorManagement';
import {
openSettingsModal,
saveSettingsModal,
saveAndCloseSettingsModal,
createTemplate,
saveTemplate
} from '../settings';
export function setupSettingsModal() {
document.getElementById('createTemplateButton').addEventListener('click', createTemplate);
document.getElementById('saveTemplateButton').addEventListener('click', saveTemplate);
document.getElementById('settingsButton').addEventListener('click', openSettingsModal);
document.getElementById('settingsSave').addEventListener('click', saveSettingsModal);
document.getElementById('settingsSaveAndClose').addEventListener('click', saveAndCloseSettingsModal);
}
export function setupIPATable(modal, textBox) {
const closeElements = modal.querySelectorAll('.modal-background, .close-button, .done-button'),
headerTextBox = modal.querySelector('header input'),
ipaButtons = modal.querySelectorAll('.td-btn button');
Array.from(closeElements).forEach(close => {
close.addEventListener('click', () => {
textBox.focus();
const endOfTextbox = textBox.value.length;
setSelectionRange(textBox, endOfTextbox, endOfTextbox);
modal.parentElement.removeChild(modal);
});
});
headerTextBox.addEventListener('change', () => {
textBox.value = headerTextBox.value;
});
Array.from(ipaButtons).forEach(button => {
button.addEventListener('click', () => {
insertAtCursor(headerTextBox, button.innerText);
textBox.value = headerTextBox.value;
});
});
setTimeout(() => {
headerTextBox.focus();
const endOfTextbox = headerTextBox.value.length;
setSelectionRange(headerTextBox, endOfTextbox, endOfTextbox);
}, 1);
}
export function setupMaximizeModal(modal, textBox) {
const closeElements = modal.querySelectorAll('.modal-background, .close-button, .done-button'),
maximizedTextBox = modal.querySelector('textarea');
Array.from(closeElements).forEach(close => {
close.addEventListener('click', () => {
const selection = getInputSelection(maximizedTextBox);
textBox.focus();
setSelectionRange(textBox, selection.start, selection.end);
modal.parentElement.removeChild(modal);
});
});
maximizedTextBox.addEventListener('change', () => {
textBox.value = maximizedTextBox.value;
})
setTimeout(() => {
const selection = getInputSelection(textBox);
maximizedTextBox.focus();
setSelectionRange(maximizedTextBox, selection.start, selection.end);
}, 1);
}
export function setupInfoModal(modal) {
const closeElements = modal.querySelectorAll('.modal-background, .close-button');
Array.from(closeElements).forEach(close => {
close.addEventListener('click', () => {
modal.parentElement.removeChild(modal);
});
});
}

View File

@ -0,0 +1,54 @@
import { renderWords } from '../render/words';
import { showSearchModal, clearSearchText, checkAllPartsOfSpeechFilters, uncheckAllPartsOfSpeechFilters } from '../search';
export 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);
}

View File

@ -0,0 +1,31 @@
import { createTemplate, saveTemplate, editSavedTemplate, deleteSelectedTemplate } from "../settings";
export function setupTemplateForm() {
document.getElementById('createTemplateButton').addEventListener('click', createTemplate);
document.getElementById('saveTemplateButton').addEventListener('click', saveTemplate);
document.getElementById('deleteTemplateButton').addEventListener('click', deleteSelectedTemplate);
setupSavedTemplatesSelect();
}
export function setupSavedTemplatesSelect() {
const savedTemplatesSelect = document.getElementById('savedDetailsTemplates');
savedTemplatesSelect.removeEventListener('change', editSavedTemplate);
savedTemplatesSelect.addEventListener('change', editSavedTemplate);
}
export function setupTemplateSelectOptions() {
const fillDetailsWithTemplate = function (e) {
if (e.target.value !== '') {
const template = window.settings.templates[parseInt(e.target.value)];
let detailsId = 'wordDetails' + e.target.id.replace('templateSelect', '');
document.getElementById(detailsId).value = template.template;
}
}
Array.from(document.getElementsByClassName('template-select')).forEach(select => {
if (select.id !== 'savedDetailsTemplates') {
select.removeEventListener('change', fillDetailsWithTemplate);
select.addEventListener('change', fillDetailsWithTemplate);
}
});
}

View File

@ -0,0 +1,119 @@
import { renderEditForm } from '../render/words';
import { confirmEditWord, cancelEditWord, confirmDeleteWord, expandAdvancedForm, submitWordForm } from '../wordManagement';
// import { goToNextPage, goToPreviousPage, goToPage } from '../pagination';
import { setupMaximizeButtons } from './buttons';
import { setupIPAFields } from '.';
export function setupWordForm() {
const wordForm = document.getElementById('wordForm'),
expandAdvancedFormButton = document.getElementById('expandAdvancedForm'),
addWordButton = document.getElementById('addWordButton');
wordForm.addEventListener('submit', event => {
// Allow semantic form and prevent it from getting submitted
event.preventDefault();
return false;
});
expandAdvancedFormButton.addEventListener('click', expandAdvancedForm);
addWordButton.addEventListener('click', submitWordForm);
setupIPAFields();
setupMaximizeButtons();
}
export function setupWordOptionButtons() {
const wordOptionButtons = document.getElementsByClassName('word-option-button');
const showWordOptions = function() {
this.parentElement.querySelector('.word-option-list').style.display = '';
}
const hideWordOptions = function(e) {
if (!e.target.classList.contains('word-option-button')) {
const allWordOptions = document.querySelectorAll('.word-option-list');
Array.from(allWordOptions).forEach(wordOptionList => {
wordOptionList.style.display = 'none';
});
}
}
Array.from(wordOptionButtons).forEach(button => {
button.removeEventListener('click', showWordOptions);
button.addEventListener('click', showWordOptions);
});
document.removeEventListener('click', hideWordOptions);
document.addEventListener('click', hideWordOptions);
}
export function setupWordOptionSelections() {
const wordOptions = document.getElementsByClassName('word-option');
Array.from(wordOptions).forEach(option => {
switch (option.innerText) {
case 'Edit': {
option.removeEventListener('click', renderEditForm);
option.addEventListener('click', renderEditForm);
break;
}
case 'Delete': {
option.removeEventListener('click', confirmDeleteWord);
option.addEventListener('click', confirmDeleteWord);
break;
}
}
});
}
export function setupWordEditFormButtons() {
const expandAdvancedFormButtons = document.getElementsByClassName('expand-advanced-form'),
saveChangesButtons = document.getElementsByClassName('edit-save-changes'),
cancelChangesButtons = document.getElementsByClassName('edit-cancel');
Array.from(expandAdvancedFormButtons).forEach(button => {
button.removeEventListener('click', expandAdvancedForm);
button.addEventListener('click', expandAdvancedForm);
});
Array.from(saveChangesButtons).forEach(button => {
button.removeEventListener('click', confirmEditWord);
button.addEventListener('click', confirmEditWord);
});
Array.from(cancelChangesButtons).forEach(button => {
button.removeEventListener('click', cancelEditWord);
button.addEventListener('click', cancelEditWord);
});
setupIPAFields();
setupMaximizeButtons();
}
export function setupMobileWordFormButton() {
const mobileButton = document.getElementById('mobileWordFormShow'),
wordForm = document.getElementById('wordForm');
mobileButton.addEventListener('click', () => {
if (mobileButton.innerText === '+') {
wordForm.style.display = 'block';
mobileButton.innerHTML = '&times;&#xFE0E;';
} else {
wordForm.style.display = '';
mobileButton.innerHTML = '+';
}
});
}
// export function setupPagination() {
// const nextButtons = document.getElementsByClassName('next-button'),
// prevButtons = document.getElementsByClassName('prev-button'),
// pageSelectors = document.getElementsByClassName('page-selector');
// Array.from(nextButtons).forEach(nextButton => {
// nextButton.removeEventListener('click', goToNextPage);
// nextButton.addEventListener('click', goToNextPage);
// });
// Array.from(prevButtons).forEach(prevButton => {
// prevButton.removeEventListener('click', goToPreviousPage);
// prevButton.addEventListener('click', goToPreviousPage);
// });
// Array.from(pageSelectors).forEach(pageSelector => {
// pageSelector.removeEventListener('change', goToPage);
// pageSelector.addEventListener('change', goToPage);
// });
// }

193
src/js/utilities.js Normal file
View File

@ -0,0 +1,193 @@
import { getCookie } from './StackOverflow/cookie';
export function getNextId() {
const lastId = window.currentDictionary.words.reduce((highestId, word) => {
return (word.wordId && word.wordId) > highestId ? word.wordId : highestId;
}, 0);
return lastId + 1;
}
export function getWordsStats() {
const {words, partsOfSpeech} = window.currentDictionary;
const {caseSensitive} = window.currentDictionary.settings;
const wordStats = {
numberOfWords: [
{
name: 'Total',
value: words.length,
},
],
wordLength: {
shortest: 0,
longest: 0,
average: 0,
},
letterDistribution: [
/* {
letter: '',
number: 0,
percentage: 0.00,
} */
],
totalLetters: 0,
};
partsOfSpeech.forEach(partOfSpeech => {
const wordsWithPartOfSpeech = words.filter(word => word.partOfSpeech === partOfSpeech);
wordStats.numberOfWords.push({
name: partOfSpeech,
value: wordsWithPartOfSpeech.length,
});
});
wordStats.numberOfWords.push({
name: 'Unclassified',
value: words.filter(word => !partsOfSpeech.includes(word.partOfSpeech)).length,
});
let totalLetters = 0;
const numberOfLetters = {};
words.forEach(word => {
const shortestWord = wordStats.wordLength.shortest;
const longestWord = wordStats.wordLength.longest;
const wordLetters = word.name.split('');
const lettersInWord = wordLetters.length;
totalLetters += lettersInWord;
if (shortestWord === 0 || lettersInWord < shortestWord) {
wordStats.wordLength.shortest = lettersInWord;
}
if (longestWord === 0 || lettersInWord > longestWord) {
wordStats.wordLength.longest = lettersInWord;
}
wordLetters.forEach(letter => {
const letterToUse = caseSensitive ? letter : letter.toLowerCase();
if (!numberOfLetters.hasOwnProperty(letterToUse)) {
numberOfLetters[letterToUse] = 1;
} else {
numberOfLetters[letterToUse]++;
}
});
});
wordStats.totalLetters = totalLetters;
wordStats.wordLength.average = words.length > 0 ? Math.round(totalLetters / words.length) : 0;
for (const letter in numberOfLetters) {
if (numberOfLetters.hasOwnProperty(letter)) {
const number = numberOfLetters[letter];
wordStats.letterDistribution.push({
letter,
number,
percentage: number / totalLetters,
});
}
}
wordStats.letterDistribution.sort((a, b) => {
if (a.percentage === b.percentage) return 0;
return (a.percentage > b.percentage) ? -1 : 1;
});
return wordStats;
}
export function wordExists(word, returnId = false) {
const { currentDictionary } = window;
const { caseSensitive } = currentDictionary.settings;
const foundWord = currentDictionary.words.find(existingWord => {
return caseSensitive ? existingWord.name === word : existingWord.name.toLowerCase() === word.toLowerCase();
});
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 addMessage(messageText, time = 5000, extraClass = false) {
const messagingSection = document.getElementById('messagingSection');
const element = document.createElement('div');
element.classList.add('message');
if (extraClass !== false) {
element.classList.add(extraClass);
}
element.innerHTML = `<a class="close-button" style="animation-duration: ${time / 1000}s;">&times;&#xFE0E;</a>` + messageText;
messagingSection.appendChild(element);
const closeButton = element.querySelector('.close-button');
const closeMessage = () => {
closeButton.removeEventListener('click', closeMessage);
fadeOutElement(element);
};
closeButton.addEventListener('click', closeMessage);
if (time > 0) {
setTimeout(closeMessage, time);
}
}
export function fadeOutElement(element) {
element.classList.add('fadeout');
setTimeout(() => {
element.parentElement.removeChild(element);
}, 300);
}
export function hideAllModals() {
const permanentModals = ['#searchModal', '#settingsModal', '#editModal'];
const hideModals = document.querySelectorAll(permanentModals.join(',')),
removeModals = document.querySelectorAll('.modal:not(' + permanentModals.join('):not(') + ')');
Array.from(hideModals).forEach(modal => modal.style.display = 'none');
Array.from(removeModals).forEach(modal => modal.parentElement.removeChild(modal));
}
export function hasToken() {
return window.isOffline !== true && getCookie('token') !== '';
}
export function objectValuesAreDifferent(newObject, oldObject) {
let valuesAreDifferent = false;
for (let property in newObject) {
if (!oldObject.hasOwnProperty(property) || JSON.stringify(newObject[property]) !== JSON.stringify(oldObject[property])) {
valuesAreDifferent = true;
}
if (typeof newObject[property] === 'object' && !Array.isArray(newObject[property])) {
valuesAreDifferent = objectValuesAreDifferent(newObject[property], oldObject[property]);
}
if (valuesAreDifferent) break;
}
return valuesAreDifferent;
}

View File

@ -0,0 +1,36 @@
import { renderDescription, renderDetails, renderStats } from './render';
export function showSection(sectionName) {
switch (sectionName) {
case 'description': showDescription(); break;
case 'details': showDetails(); break;
case 'stats': showStats(); break;
}
}
export function hideDetailsPanel() {
document.getElementById('detailsPanel').style.display = 'none';
}
export function getIsDetailsPanelDisplayed() {
return document.getElementById('detailsPanel').style.display !== 'none';
}
function showDescription() {
const detailsPanel = document.getElementById('detailsPanel');
detailsPanel.style.display = 'block';
renderDescription();
}
function showDetails() {
const detailsPanel = document.getElementById('detailsPanel');
detailsPanel.style.display = 'block';
renderDetails();
}
function showStats() {
const detailsPanel = document.getElementById('detailsPanel');
detailsPanel.style.display = 'block';
renderStats();
}

12
src/js/view/index.js Normal file
View File

@ -0,0 +1,12 @@
import { renderAll } from './render';
import setupListeners from './setupListeners';
function initialize() {
renderAll();
setupListeners();
}
window.onload = (function (oldLoad) {
oldLoad && oldLoad();
initialize();
})(window.onload);

249
src/js/view/render.js Normal file
View File

@ -0,0 +1,249 @@
import md from 'marked';
import { removeTags, slugify } from '../../helpers';
import { getHomonymnNumber } from './utilities';
import { getMatchingSearchWords, highlightSearchTerm, getSearchFilters, getSearchTerm } from './search';
import { showSection } from './displayToggles';
import { setupInfoModal } from '../setupListeners/modals';
import { setupSearchFilters } from '../setupListeners/search';
export function renderAll() {
renderTheme();
renderCustomCSS();
renderDictionaryDetails();
renderPartsOfSpeech();
renderWords();
}
export function renderTheme() {
const { theme } = window.currentDictionary.settings;
document.body.id = theme + 'Theme';
}
export function renderCustomCSS() {
const { customCSS } = window.currentDictionary.settings;
const stylingId = 'customCSS';
const stylingElement = document.getElementById(stylingId);
if (!stylingElement) {
const styling = document.createElement('style');
styling.id = stylingId;
styling.innerHTML = customCSS;
document.body.appendChild(styling);
} else {
stylingElement.innerHTML = customCSS;
}
}
export function renderDictionaryDetails() {
renderName();
showSection('description');
}
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() {
const descriptionHTML = md(window.currentDictionary.description);
document.getElementById('detailsPanel').innerHTML = '<div class="content">' + descriptionHTML + '</div>';
}
export function renderDetails() {
const { partsOfSpeech, alphabeticalOrder } = window.currentDictionary;
const { phonology, phonotactics, orthography, grammar } = window.currentDictionary.details;
const partsOfSpeechHTML = `<p><strong>Parts of Speech</strong><br>${partsOfSpeech.map(partOfSpeech => '<span class="tag">' + partOfSpeech + '</span>').join(' ')}</div>`;
const alphabeticalOrderHTML = `<p><strong>Alphabetical Order</strong><br>${
(alphabeticalOrder.length > 0 ? alphabeticalOrder : ['English Alphabet']).map(letter => `<span class="tag">${letter}</span>`).join(' ')
}</div>`;
const generalHTML = `<h3>General</h3>${partsOfSpeechHTML}${alphabeticalOrderHTML}`;
const { consonants, vowels, blends } = phonology
const consonantHTML = `<p><strong>Consonants</strong><br>${consonants.map(letter => `<span class="tag">${letter}</span>`).join(' ')}</p>`;
const vowelHTML = `<p><strong>Vowels</strong><br>${vowels.map(letter => `<span class="tag">${letter}</span>`).join(' ')}</p>`;
const blendHTML = blends.length > 0 ? `<p><strong>Polyphthongs&nbsp;/&nbsp;Blends</strong><br>${blends.map(letter => `<span class="tag">${letter}</span>`).join(' ')}</p>` : '';
const phonologyNotesHTML = phonology.notes.trim().length > 0 ? '<p><strong>Notes</strong></p><div>' + md(phonology.notes) + '</div>' : '';
const phonologyHTML = `<h3>Phonology</h3>
<div class="split two">
<div>${consonantHTML}</div>
<div>${vowelHTML}</div>
</div>
${blendHTML}
${phonologyNotesHTML}`;
const { onset, nucleus, coda } = phonotactics;
const onsetHTML = `<p><strong>Onset</strong><br>${onset.map(letter => `<span class="tag">${letter}</span>`).join(' ')}</p>`;
const nucleusHTML = `<p><strong>Nucleus</strong><br>${nucleus.map(letter => `<span class="tag">${letter}</span>`).join(' ')}</p>`;
const codaHTML = `<p><strong>Coda</strong><br>${coda.map(letter => `<span class="tag">${letter}</span>`).join(' ')}</p>`;
const phonotacticsNotesHTML = phonotactics.notes.trim().length > 0 ? '<p><strong>Notes</strong></p><div>' + md(phonotactics.notes) + '</div>' : '';
const phonotacticsHTML = onset.length + nucleus.length + coda.length + phonotacticsNotesHTML.length > 0
? `<h3>Phonotactics</h3>
${onset.length > 0 || nucleus.length > 0 || coda.length > 0
? `<div class="split three">
<div>${onsetHTML}</div>
<div>${nucleusHTML}</div>
<div>${codaHTML}</div>
</div>` : ''}
${phonotacticsNotesHTML}`
: '';
const { translations } = orthography;
const translationsHTML = translations.length > 0 ? `<p><strong>Translations</strong><br>${translations.map(translation => {
translation = translation.split('=').map(value => value.trim());
if (translation.length > 1 && translation[0] !== '' && translation[1] !== '') {
return `<span><span class="tag">${translation[0]}</span><span class="tag orthographic-translation">${translation[1]}</span></span>`;
}
return false;
}).filter(html => html !== false).join(' ')}</p>` : '';
const orthographyNotesHTML = orthography.notes.trim().length > 0 ? '<p><strong>Notes</strong><br>' + md(orthography.notes) + '</div>' : '';
const orthographyHTML = translations.length + orthographyNotesHTML.length > 0
? `<h3>Orthography</h3>
${translationsHTML}
${orthographyNotesHTML}`
: '';
const grammarHTML = grammar.notes.trim().length > 0
? '<h3>Grammar</h3><div>'
+ (grammar.notes.trim().length > 0 ? md(grammar.notes) : '')
+ '</div>'
: '';
detailsPanel.innerHTML = generalHTML + phonologyHTML + phonotacticsHTML + orthographyHTML + grammarHTML;
}
export function renderStats() {
const { wordStats } = window.currentDictionary;
const numberOfWordsHTML = `<p><strong>Number of Words</strong><br>${wordStats.numberOfWords.map(stat => `<span><span class="tag">${stat.name}</span><span class="tag">${stat.value}</span></span>`).join(' ')}</p>`;
const wordLengthHTML = `<p><strong>Word Length</strong><br><span><span class="tag">Shortest</span><span class="tag">${wordStats.wordLength.shortest}</span></span>
<span><span class="tag">Longest</span><span class="tag">${wordStats.wordLength.longest}</span></span>
<span><span class="tag">Average</span><span class="tag">${wordStats.wordLength.average}</span></span></p>`;
const letterDistributionHTML = `<p><strong>Letter Distribution</strong><br>${wordStats.letterDistribution.map(stat => `<span title="${stat.number} ${stat.letter}'s total"><span class="tag">${stat.letter}</span><span class="tag">${stat.percentage.toFixed(2)}</span></span>`).join(' ')}</p>`;
const totalLettersHTML = `<p><strong>${wordStats.totalLetters} Total Letters</strong></p>`;
detailsPanel.innerHTML = numberOfWordsHTML + wordLengthHTML + letterDistributionHTML + totalLettersHTML;
}
export function renderPartsOfSpeech(onlyOptions = false) {
let optionsHTML = '<option value=""></option>',
searchHTML = '<label>Unclassified <input type="checkbox" checked id="searchPartOfSpeech__None"></label>';
window.currentDictionary.partsOfSpeech.forEach(partOfSpeech => {
partOfSpeech = removeTags(partOfSpeech);
optionsHTML += `<option value="${partOfSpeech.replace(/"/g, '&quot;')}">${partOfSpeech}</option>`;
searchHTML += `<label>${partOfSpeech} <input type="checkbox" checked id="searchPartOfSpeech_${slugify(partOfSpeech)}"></label>`;
});
searchHTML += `<a class="small button" id="checkAllFilters">Check All</a> <a class="small button" id="uncheckAllFilters">Uncheck All</a>`;
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 = `<article class="entry">
<header>
<h4 class="word">No Words Found</h4>
</header>
<dl>
<dt class="definition">Either this dictionary has not yet been started, or something prevented words from downloading.</dt>
</dl>
</article>`;
} else {
words = getMatchingSearchWords();
if (words.length === 0) {
wordsHTML = `<article class="entry">
<header>
<h4 class="word">No Search Results</h4>
</header>
<dl>
<dt class="definition">Edit your search or filter to show words.</dt>
</dl>
</article>`;
}
words.forEach(originalWord => {
const word = highlightSearchTerm({
name: removeTags(originalWord.name),
pronunciation: removeTags(originalWord.pronunciation),
partOfSpeech: removeTags(originalWord.partOfSpeech),
definition: removeTags(originalWord.definition),
details: originalWord.details,
etymology: typeof originalWord.etymology === 'undefined' || originalWord.etymology.length < 1 ? null
: originalWord.etymology.join(', '),
related: typeof originalWord.related === 'undefined' || originalWord.related.length < 1 ? null
: originalWord.related.join(', '),
principalParts: typeof originalWord.principalParts === 'undefined' || originalWord.principalParts.length < 1 ? null
: originalWord.principalParts.join(', '),
wordId: originalWord.wordId,
});
const homonymnNumber = getHomonymnNumber(originalWord);
const shareLink = window.location.pathname + (window.location.pathname.match(new RegExp(word.wordId + '$')) ? '' : '/' + word.wordId);
wordsHTML += `<article class="entry" id="${word.wordId}">
<header>
<h4 class="word"><span class="orthographic-translation">${word.name}</span>${homonymnNumber > 0 ? ' <sub>' + homonymnNumber.toString() + '</sub>' : ''}</h4>
${word.principalParts === null ? '' : `<span class="principalParts">(${word.principalParts})</span>`}
<span class="pronunciation">${word.pronunciation}</span>
<span class="part-of-speech">${word.partOfSpeech}</span>
<a href="${shareLink}" target="_blank" class="small button word-option-button" title="Link to Word">&#10150;</a>
</header>
<dl>
<dt class="definition">${word.definition}</dt>
<dd class="details">
${md(word.details)}
</dd>
${word.etymology === null && word.related === null ? '' : `<hr>`}
${word.etymology === null ? '' : `<dt>Etymology <small>(Root Word${originalWord.etymology.length !== 1 ? 's' : ''})</small></dt>
<dd class="etymology">
${md(word.etymology).replace(/<\/?p>/g, '')}
</dd>`}
${word.related === null ? '' : `<dt>Related Word${originalWord.related.length !== 1 ? 's' : ''}</dt>
<dd class="related">
${md(word.related).replace(/<\/?p>/g, '')}
</dd>`}
</dl>
</article>`;
});
}
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 = `<div class="modal-background"></div>
<div class="modal-content">
<a class="close-button">&times;&#xFE0E;</a>
<section class="info-modal">
<div class="content">
${content}
</div>
</section>
</div>`;
document.body.appendChild(modalElement);
setupInfoModal(modalElement);
}

168
src/js/view/search.js Normal file
View File

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

View File

@ -0,0 +1,102 @@
import {showSection, hideDetailsPanel} from './displayToggles';
import { showSearchModal, clearSearchText, checkAllPartsOfSpeechFilters, uncheckAllPartsOfSpeechFilters } from './search';
import { renderWords, renderInfoModal } from './render';
import helpFile from '../../markdown/help.md';
import termsFile from '../../markdown/terms.md';
import privacyFile from '../../markdown/privacy.md';
export default function setupListeners() {
setupDetailsTabs();
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);
}
});
});
}
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', () => {
renderInfoModal(helpFile);
});
document.getElementById('termsInfoButton').addEventListener('click', () => {
renderInfoModal(termsFile);
});
document.getElementById('privacyInfoButton').addEventListener('click', () => {
renderInfoModal(privacyFile);
});
}
export function setupInfoModal(modal) {
const closeElements = modal.querySelectorAll('.modal-background, .close-button');
Array.from(closeElements).forEach(close => {
close.addEventListener('click', () => {
modal.parentElement.removeChild(modal);
});
});
}

28
src/js/view/utilities.js Normal file
View File

@ -0,0 +1,28 @@
export function getHomonymnIndexes(word) {
const { currentDictionary } = window;
const { caseSensitive } = currentDictionary.settings;
const foundIndexes = [];
currentDictionary.words.forEach((existingWord, index) => {
if (existingWord.wordId !== word.wordId
&& (caseSensitive ? existingWord.name === word.name : existingWord.name.toLowerCase() === word.name.toLowerCase())) {
foundIndexes.push(index);
}
});
return foundIndexes;
}
export function getHomonymnNumber(word) {
const homonyms = getHomonymnIndexes(word);
if (homonyms.length > 0) {
const index = window.currentDictionary.words.findIndex(w => w.wordId === word.wordId);
let number = 1;
for (let i = 0; i < homonyms.length; i++) {
if (index < homonyms[i]) break;
number++;
}
return number;
}
return 0;
}

View File

@ -0,0 +1,11 @@
import removeDiacritics from "../StackOverflow/removeDiacritics";
export function sortWords() {
const { sortByDefinition } = window.currentDictionary.settings;
const sortBy = sortByDefinition ? 'definition' : 'name';
window.currentDictionary.words.sort((wordA, wordB) => {
if (removeDiacritics(wordA[sortBy]).toLowerCase() === removeDiacritics(wordB[sortBy]).toLowerCase()) return 0;
return removeDiacritics(wordA[sortBy]).toLowerCase() > removeDiacritics(wordB[sortBy]).toLowerCase() ? 1 : -1;
});
}

385
src/js/wordManagement.js Normal file
View File

@ -0,0 +1,385 @@
import { renderWords, renderWord } from "./render/words";
import { wordExists, addMessage, getNextId, hasToken, getHomonymnIndexes } from "./utilities";
import removeDiacritics from "./StackOverflow/removeDiacritics";
import { removeTags, getTimestampInSeconds } from "../helpers";
import { saveDictionary } from "./dictionaryManagement";
import { setupWordOptionButtons, setupWordOptionSelections } from "./setupListeners/words";
import { wordMatchesSearch } from "./search";
export function validateWord(word, wordId = false) {
const errorElementId = wordId === false ? 'wordErrorMessage' : 'wordErrorMessage_' + wordId,
errorElement = document.getElementById(errorElementId);
let errorMessage = '';
if (word.name === '') {
errorMessage += '<p class="bold red">Word field must not be blank.</p>';
}
if (word.definition === '' && word.details === '') {
errorMessage += '<p class="bold red">You must enter Definition or Details.</p>';
}
const { allowDuplicates, caseSensitive } = window.currentDictionary.settings;
if (!allowDuplicates) {
const foundDuplicate = wordExists(word.name, true);
if (foundDuplicate !== false) {
if (wordId !== false && foundDuplicate !== wordId) {
errorMessage += `<p class="bold red">"<a href="#${foundDuplicate}">${word.name}</a>" already exists, and "Prevent Duplicate Words" is turned on.${!caseSensitive ? ' <em>(Case sensitivity is turned off)</em>' : ''}</p>`;
}
}
}
errorElement.innerHTML = errorMessage;
return errorMessage === '';
}
export function sortWords(render) {
const { sortByDefinition } = window.currentDictionary.settings;
const { alphabeticalOrder } = window.currentDictionary;
const sortBy = sortByDefinition ? 'definition' : 'name';
window.currentDictionary.words.sort((wordA, wordB) => {
// 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);
if (render) {
renderWords();
}
}
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 => {
const wordMarkdownLink = getWordReferenceMarkdown(reference);
if (wordMarkdownLink !== reference) {
detailsMarkdown = detailsMarkdown.replace(new RegExp(reference, 'g'), wordMarkdownLink);
}
});
}
return detailsMarkdown;
}
export function getWordReferenceMarkdown(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 = homonymnIndexes.length > 1 && homonymn - 1 >= 0 ? '<sub>' + homonymn.toString() + '</sub>' : '';
return `<span class="word-reference">[<span class="orthographic-translation">${translateOrthography(wordToFind)}</span>${homonymnSubHTML}](#${existingWordId})</span>`;
}
return reference;
}
export function expandAdvancedForm(id = false) {
const wordId = typeof id.target !== 'undefined' ? this.id.replace('expandAdvancedForm', '') : id;
const button = typeof id.target !== 'undefined' ? this : document.getElementById('expandAdvancedForm' + (!wordId ? '' : wordId)),
form = document.getElementById('advancedForm' + (!wordId ? '' : wordId));
if (form.style.display !== 'block') {
button.innerText = 'Hide Advanced Fields';
form.style.display = 'block';
} else {
button.innerText = 'Show Advanced Fields';
form.style.display = 'none';
}
}
export function submitWordForm() {
const name = document.getElementById('wordName').value,
pronunciation = document.getElementById('wordPronunciation').value,
partOfSpeech = document.getElementById('wordPartOfSpeech').value,
definition = document.getElementById('wordDefinition').value,
details = document.getElementById('wordDetails').value,
etymology = document.getElementById('wordEtymology').value,
related = document.getElementById('wordRelated').value,
principalParts = document.getElementById('wordPrincipalParts').value;
const word = {
name: removeTags(name).trim(),
pronunciation: removeTags(pronunciation).trim(),
partOfSpeech: removeTags(partOfSpeech).trim(),
definition: removeTags(definition).trim(),
details: removeTags(details).trim(),
wordId: getNextId(),
};
if (removeTags(etymology).trim() !== '') {
word.etymology = removeTags(etymology).split(',').map(w => w.trim()).filter(w => w.length > 0);
}
if (removeTags(related).trim() !== '') {
word.related = removeTags(related).split(',').map(w => w.trim()).filter(w => w.length > 0);
}
if (removeTags(principalParts).trim() !== '') {
word.principalParts = removeTags(principalParts).split(',').map(w => w.trim()).filter(w => w.length > 0);
}
if (validateWord(word)) {
addWord(word);
sortWords(true);
if (hasToken()) {
import('./account/index.js').then(account => {
account.uploadWord(word);
});
}
clearWordForm();
}
}
export function clearWordForm() {
document.getElementById('wordName').value = '';
document.getElementById('wordPronunciation').value = '';
document.getElementById('wordPartOfSpeech').value = '';
document.getElementById('wordDefinition').value = '';
document.getElementById('wordDetails').value = '';
document.getElementById('templateSelect').value = '';
document.getElementById('wordEtymology').value = '';
document.getElementById('wordRelated').value = '';
document.getElementById('wordPrincipalParts').value = '';
document.getElementById('wordName').focus();
}
export function addWord(word, message = true) {
const timestamp = getTimestampInSeconds();
word.lastUpdated = timestamp;
word.createdOn = timestamp;
window.currentDictionary.words.push(word);
if (message) {
addMessage(`<a href="#${word.wordId}">${word.name}</a> Created Successfully`, 30000);
}
return word;
}
export function deleteWord(wordId) {
const wordIndex = window.currentDictionary.words.findIndex(word => word.wordId === wordId);
if (wordIndex < 0) {
console.error('Could not find word to delete');
addMessage('Could not find word to delete. Please refresh your browser and try again.', 10000, 'error');
} else {
window.currentDictionary.words.splice(wordIndex, 1);
addMessage('Word Deleted Successfully');
sortWords(true);
if (hasToken()) {
import('./account/index.js').then(account => {
account.deleteWord(wordId);
});
}
}
}
export function updateWord(word, wordId) {
const wordIndex = window.currentDictionary.words.findIndex(word => word.wordId === wordId);
if (wordIndex < 0) {
console.error('Could not find word to update');
addMessage('Could not find word to update. Please refresh your browser and try again.', 10000, 'error');
} else {
const isPublic = hasToken() && window.currentDictionary.settings.isPublic;
const { sortByDefinition } = window.currentDictionary.settings;
const existingWord = window.currentDictionary.words[wordIndex];
const needsReRender = (sortByDefinition && word.definition !== existingWord.definition)
|| (!sortByDefinition && word.name !== existingWord.name);
word.lastUpdated = getTimestampInSeconds();
word.createdOn = existingWord.createdOn;
window.currentDictionary.words[wordIndex] = word;
addMessage('Word Updated Successfully');
if (needsReRender) {
sortWords(true);
} else {
saveDictionary(false);
const entry = document.getElementById(wordId.toString());
if (!wordMatchesSearch(word)) {
entry.parentElement.removeChild(entry);
} else {
console.log('matches search, updating in place');
document.getElementById(wordId.toString()).outerHTML = renderWord(window.currentDictionary.words[wordIndex], isPublic);
setupWordOptionButtons();
setupWordOptionSelections();
}
}
if (hasToken()) {
import('./account/index.js').then(account => {
account.uploadWord(word);
});
}
}
}
export function confirmEditWord(id) {
const wordId = typeof id.target !== 'undefined' ? parseInt(this.id.replace('editWordButton_', '')) : id;
const name = document.getElementById('wordName_' + wordId).value,
pronunciation = document.getElementById('wordPronunciation_' + wordId).value,
partOfSpeech = document.getElementById('wordPartOfSpeech_' + wordId).value,
definition = document.getElementById('wordDefinition_' + wordId).value,
details = document.getElementById('wordDetails_' + wordId).value,
etymology = document.getElementById('wordEtymology_' + wordId).value,
related = document.getElementById('wordRelated_' + wordId).value,
principalParts = document.getElementById('wordPrincipalParts_' + wordId).value;
const word = {
name: removeTags(name).trim(),
pronunciation: removeTags(pronunciation).trim(),
partOfSpeech: removeTags(partOfSpeech).trim(),
definition: removeTags(definition).trim(),
details: removeTags(details).trim(),
wordId,
};
if (removeTags(etymology).trim() !== '') {
word.etymology = removeTags(etymology).split(',').map(w => w.trim()).filter(w => w.length > 0);
}
if (removeTags(related).trim() !== '') {
word.related = removeTags(related).split(',').map(w => w.trim()).filter(w => w.length > 0);
}
if (removeTags(principalParts).trim() !== '') {
word.principalParts = removeTags(principalParts).split(',').map(w => w.trim()).filter(w => w.length > 0);
}
if (validateWord(word, wordId)) {
if (confirm(`Are you sure you want to save changes to "${word.name}"?`)) {
document.getElementById('editForm_' + wordId).classList.add('done');
updateWord(word, wordId);
}
}
}
export function cancelEditWord() {
const wordId = parseInt(this.parentElement.id.replace('editForm_', ''));
if (confirm(`Are you sure you want to cancel?\n(Any changes will be lost!)`)) {
document.getElementById('editForm_' + wordId).classList.add('done');
renderWords();
}
}
export function confirmDeleteWord(wordId) {
wordId = typeof wordId.target === 'undefined' ? wordId : parseInt(wordId.target.id.replace('delete_', ''));
const word = window.currentDictionary.words.find(w => w.wordId === wordId);
if (!word) {
console.error('Something went wrong! Couldn\'t find word with id of ' + wordId);
addMessage('Could not find word to delete. Please refresh your browser and try again.', 10000, 'error');
} else {
if (confirm(`Are you sure you want to delete "${word.name}"?`)) {
if (confirm(`Just to double-check:\nDo you really want to delete "${word.name}"?\n\nYou won't be able to undo it!`)) {
deleteWord(wordId);
}
}
}
}
export function getOpenEditForms() {
const openEditForms = document.getElementsByClassName('edit-form');
const formsToReopen = [];
Array.from(openEditForms).forEach(form => {
if (!form.classList.contains('done')) {
formsToReopen.push(parseInt(form.id.replace('editForm_', '')));
}
});
return formsToReopen;
}

21
src/main.scss Normal file
View File

@ -0,0 +1,21 @@
@import 'scss/variables';
@import '../node_modules/normalize.css/normalize.css';
@import 'scss/containers';
@import 'scss/structure';
@import 'scss/elements';
@import 'scss/mobile/containers';
@import 'scss/mobile/structure';
@import 'scss/mobile/elements';
@import 'scss/themes/default';
@import 'scss/themes/dark';
@import 'scss/themes/light';
@import 'scss/themes/blue';
@import 'scss/themes/green';
@import 'scss/themes/royal';
@import 'scss/themes/yellow';
@import 'scss/themes/red';
@import 'scss/themes/mint';
@import 'scss/themes/grape';

263
src/markdown/help.md Normal file
View File

@ -0,0 +1,263 @@
# Lexiconga Help
## Table of Contents
* [What is Lexiconga?](#what-is-lexiconga)
* [How do I use Lexiconga?](#how-do-i-use-lexiconga)
* [Getting Started](#getting-started)
* [Viewing your Dictionary's Details](#viewing-your-dictionarys-details)
* [Referencing Other Words](#referencing-other-words)
* [Maximizing Large Text Boxes](#maximizing-large-text-boxes)
* [IPA Auto-Fill Fields](#ipa-auto-fill-fields)
* [Advanced Fields](#advanced-fields)
* [Entry Management](#entry-management)
* [Search/Filter](#searchfilter)
* [The Settings Window](#the-settings-window)
* [The Dictionary Settings Window](#the-dictionary-settings-window)
* [Keyboard Shortcuts](#keyboard-shortcuts)
* [Accounts](#accounts)
* [Creating An Account](#creating-an-account)
* [Logging In](#logging-in)
* [Differences](#differences)
* [Settings](#settings-1)
* [Public Dictionaries](#public-dictionaries)
* [Forgot Your Password?](#forgot-your-password)
* [Lockout](#lockout)
* [Problems or Requests](#problems-or-requests)
* [Update Log](#update-log)
* [Open Source](#open-source)
* [Thanks](#thanks)
## What is Lexiconga?
Lexiconga is a tool built to help you build constructed language (conlang) dictionaries/lexicons quickly and easily.
You can enter words and definitions, and they will appear nicely formatted and in alphabetical order by name under your dictionary's title and details. You can also set your dicitonary to sort your words by definition if you prefer that view or even specify a fully custom alphabetical order. If the default parts of speech are not adequate for your conlang, you can change them to whatever you might need. You can also enter a description and full set of language rules that you can toggle on and off below the dictionary's title!
Lexiconga accepts Unicode characters so you can utilize whatever typable characters you might need and [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) for formatting long text entries, and if you want to share or even just make a backup of your dictionary, you can export it to a single convenient file that can be easily re-imported. Your dictionary is saved to your browser's [localStorage](https://www.w3schools.com/html/html5_webstorage.asp) every time you make a change, which means as long as you use the same browser and don't deliberately delete it by clearing your cache, your dictionary will always be there when you come back.
If you would like an added layer of accessibility and security (in case you clear your browser cache frequently), you can create an account, where you can store and switch between as many dictionaries as you need. Having an account will also allow you to access your dictionaries from any browser by logging in.
## How do I use Lexiconga?
### Getting Started
When you have a brand new, empty dictionary, the first thing you'll probably want to do is change the title to whatever your conlang is called and add at least a little description of what your language is like or how to use it. You can do this by clicking on the **Edit** button, which will open up the Dictionary Settings screen. Here, you will find all the fields you need to update your dictionary's Name, Specification, and Description. The Description text area uses [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) to format any text you include, so brush up on how to do basic things in Markdown before you get started _(NOTE: a line break is created by adding 2 or more spaces to the end of the line and then going to the next line!)_.
After this, go to the **Details** tab and make sure that the Parts of Speech are adequate for your language _(see below for more information about this)_, and add phonology information if you'd like _(learning to use the [International Phonetic Alphabet](https://en.wikipedia.org/wiki/International_Phonetic_Alphabet) can be very helpful here)_. Update these fields how you want them and click the "Save" button to keep the Dictionary Settings menu open, or the "Save & Close" button to close the menu and start adding words!
To add words, use the form on the top left side of the window _(in mobile, click the **+** button to show the form)_. Hopefully the form is self-explanatory, but if not, here's a little guide:
Enter the word in your constructed language in the "Word" field, the pronunciation of the word in the "Pronunciation" field, choose a Part of Speech, enter an definition/equivalent word in the "Definition" field and/or a longer definition or fuller explanation of the word using [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) in the "Details" text area, and click "Add Word". Your word will instantly appear in your dictionary under the dictionary's name! You can add as many words as you want this way.
The only things to remember while adding new words is that the minimum information that you can enter is the Word itself and either the Definition OR the Details. You can have both of these or just one, but you need at least one. If you do not want to use the Pronunciation or Parts of Speech then you do not need to, though if you leave out Part of Speech, you will miss out on the handy Search Filter feature.
And that's all you need to get started! Everything else should be pretty self-explanatory, but a full explanation of Lexiconga and all of its functions continues below.
### Viewing your Dictionary's Details
After you enter a markdown-formatted description/rules in the Dictionary Settings menu, you can view the formatted version by clicking the "Description" button under your dictionary's name. You can hide it again by clicking "Description" when the description is displayed. The other buttons in the row also display and hide their information.
### 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.
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 &times; 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)!
### IPA Auto-Fill Fields
You may notice some buttons around the Pronunciation field in the main word form and some other fields in the Details tab of your Dictionary Settings menu. This indicates that the field is using a special feature that generates [International Phonetic Alphabet](https://en.wikipedia.org/wiki/International_Phonetic_Alphabet) (IPA) characters when typing certain combinations of characters. Click the "Field Help" button below the field for instructions on how to use those fields.
You can also click the "IPA Table" button to display a maximized table that shows all of the characters in the IPA. Clicking them will add it to the pronunciation field wherever you position the cursor in the field. If you hover over the button for an IPA character that is not on a standard keyboard, you can what character combinations will produce that character when you type it in the field.
If you do not want to use the IPA field feaure, you can turn it off in the Settings. Click the "Settings" button in the top right side of the website, uncheck "Use IPA Auto-Fill", and click "Save" or "Save & Close". The "IPA Table" and "Field Help" buttons will be removed from all fields they were around, and you can use those fields as normal.
### Advanced Fields
Clicking the button labeled "Show Advanced Fields" underneath the **Details** field on any word form will expand the "Advanced Fields" section for that word list. Clicking the button again when it says "Hide Advanced Fields" will hide the section again to make it so you don't have to scroll down to reach the "Add Word" button. The fields in the Advanced Fields section are detailed below:
- **Details Field Templates:** A dropdown box with any templates you have previously created (see [Templates for Details Fields](#templates-for-details-fields) in the Settings section below to learn how to create a template). Selecting one of the templates will put that template into the Details field for you, _overwriting **anything** that might have already been written there_.
- If there are no templates created, this field will not be displayed.
- Each time you select a different template (other than the "None Selected" option), it will overwrite anything in the Details field with the template without warning, so please be careful.
- Selecting the "None Selected" option at the top of the list will _not_ change the Details field.
- **Etymology / Root Words:** Any words that the current word might have stemmed from originally.
- Words written here are treated as word references (see [Referencing Other Words](#referencing-other-words) for how to reference a word or a duplicate word) without requiring the \{\{double-curly-braces\}\} as in the Details field. The words referenced here will appear below the word's details in the list.
- Separate each individual root word with a comma.
- **Related Words:** Any words that might be related to the current word.
- Words written here are treated as word references (see [Referencing Other Words](#referencing-other-words) for how to reference a word or a duplicate word) without requiring the \{\{double-curly-braces\}\} as in the Details field. The words referenced here will appear below the word's details in the list.
- Separate each individual related word with a comma.
- **Principal Parts:** Any words that someone must know in order to conjugate the current word (see the [Wikipedia entry](https://en.wikipedia.org/wiki/Principal_parts) for more details).
- Words written here are treated as word references (see [Referencing Other Words](#referencing-other-words) for how to reference a word or a duplicate word) without requiring the \{\{double-curly-braces\}\} as in the Details field. The words referenced here will appear below the word's details in the list.
- Separate each individual principal part with a comma.
### Entry Management
After adding some words to your dictionary, you'll see an "Options" button on each entry. Clicking the button reveals Edit and Delete buttons.
The **Edit** button will cause a form with the current details of the word you edited to display in the same space that the word previously appeared. You can make any changes you want and click the "Save Changes" button. You will be asked to confirm your changes, and once you do, your word will be saved. If you do not want to make changes, just click the "Cancel" button.
The **Delete** button will ask you to confirm that you want to delete the entry, and if you say yes, the word will be _permanently deleted and **cannot be retrieved**_.
Note that to use these fields, your browser will need to allow "confirm" and "prompt" popups. Preventing these fuctions will prevent words from being edited or deleted.
### Search/Filter
You can search entries or filter by part of speech by clicking the "Search" button at the top of the screen. This will open a small panel that will stick to the top of your screen as you scroll.
From there, you can enter any text you want in the search box and either press Enter or click anywhere outside the search box, and Lexiconga will display any and every entry including your entry. To display the entire dictionary again, you must clear the search box.
You can refine your search by clicking the "Toggle Options" button and using the checkboxes below the search box. There are 3 sections that can help you filter your search:
- **Search For**
- **Case-Sensitive:** When checked, Lexiconga finds entries matching the letter case in the entered text. When unchecked, it will find any case as long as the letters match.
- **Ignore Diacritics/Accents:** When checked, Lexiconga will ignore accented letters and diacritics and identify them as their equivalent unaccented letter and vice-versa, in case you want to find a word with a diacritic without entering the diacritic in the search box. When unchecked, it will only find diacritics and accented letters if they are specifically entered in the search box.
- **Exact Words:** When checked, the search term will find entries with _exact matches_ in only the Word or Definition field. If Word or Definition has _any_ text aside from exactly what was entered in the search bar, it will not be displayed.
- **Translation:** When checked, Lexiconga will translate all words and references by any specified orthographic translations and compare your search term with that instead of the words as entered.
- **Include in Search**
- **Word**: When checked, Lexiconga searches your dictionary's "Word" entries for the entered text. When unchecked, it ignores it.
- **Definition**: When checked, Lexiconga searches your dictionary's "Definition/Equivalent Word(s)" entries for the entered text. When unchecked, it ignores it.
- **Explanation**: When checked, Lexiconga searches your dictionary's "Explanation/Long Definition" entries for the entered text. When unchecked, it ignores it.
- **Include Only:** Lists all parts of speech and "Unclassified" with a checkbox next to each. If you uncheck any of the parts of speech, any entries in your dictionary with that part of speech will not be shown. Use the "Check All" and "Uncheck All" buttons to quickly select/unselect all of them.
- **Unclassified:** Checking this box will show entries that do not have a part of speech.
When you have a search term or filter applied, you can see the number of results and "(Filtered)" next to the search button. To close the search panel, click the &times; button or the darker space on either side of it.
To display _all_ of your words again, clear your search bar and ensure all the "Include Only" checkboxes are checked.
### The Settings Window
Clicking the "Settings" button in the top-right side of Lexiconga will show the Settings window with some options.
- **Use IPA Auto-Fill:** Check this to use character combinations to input International Phonetic Alphabet characters into Pronunciation fields. Use the "Field Help" button for instructions on how to use it and the "IPA Table" to display available characters. Uncheck it to disable the feature and hide the buttons.
- **Use Hotkeys:** Check this to enable keyboard combinations to perform different helpful actions (see [Keyboard Shortcuts](#keyboard-shortcuts) below). Unchecking this disables the feature.
- Note: If your browser does not support required features, this will be disabled automatically.
- **Show Advanced Fields By Default:** Check this to make the advanced fields show on word forms without needing to click the "Show Advanced Fields" button (see [Advanced Fields](#advanced-fields) above). Unchecking this makes it so you need to click the "Show Advanced Fields" button to show the fields each time you edit a word.
- **Default Theme:** Choose what color theme new dictionaries will use when they are created.
After making changes, click the "Save" or "Save & Close" button to save your changes.
#### Templates for Details Fields
Below the "Default Theme" selector is the template editor. Either click "Create New Template" or choose one from the "Saved Templates" dropdown to begin editing the template. Once a new template is created or a saved template is selected, new fields will appear labeled "Template Name" and "Template," plus a "Save Template" and "Delete Template" button.
You can modify the template's name by editing the "Template Name" field, and whatever you set in the "Template" text area will be used as the template in the Details field of a word form if it is selected (see [Advanced Fields](#advanced-fields) above for more about how this is used).
After making any changes to a template, click the "Save Template" button below the "Template" field to save it to your browser. Templates are only stored in the browser they were created on and _do not_ get uploaded to your account (if you have one). If you create a template, it will _not be available on any other web browser_ unless you re-create it!
Clicking the "Delete Template" button will prompt you to confirm that you want to delete it, and if you confirm, it will permanently delete the currently selected template from your browser. There is no way to recover deleted templates!
### The Dictionary Settings Window
Clicking the "Edit" button under your dictionary's name will display a window with tabs that each contain different fields and options.
#### Description
- **Name:** Your dictionary's name. Displays above the entries.
- **Specification:** How your dictionary is referred to. For example, you can change this to something like "Word List" or "Lexicon" if you prefer. Displays above the entries after Name.
- **Description:** Any information you want to enter about your dictionary. Uses [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) formatting.
#### Details
- **Parts of Speech:** The parts of speech available in the dropdown box on word forms. Separate each individual part of speech with a comma.
- **Alphabetical Order:** The order that your words will be sorted by. Include every letter and different capitalization used in your dictionary to sort your words in whatever order you want—any letters in your words that are not sorted here are sorted by the default ASCII/Unicode order (i.e. English Alphabetical) _after_ any custom-sorted words. Lexiconga can only sort by single characters (rather than sets of characters) and will sort the words _as entered_, not using orthographic translations. Separate each character with a _space_.
- **Phonology**
- **Consonants:** The IPA characters representing the consonants present in your language. Uses the IPA Auto-Fill feature unless it is turned off. Separate each consonant with a _space_ so they will be displayed correctly under the Details section of your dictionary.
- **Vowels:** The IPA characters representing the vowels present in your language. Uses the IPA Auto-Fill feature unless it is turned off. Separate each vowel with a _space_ so they will be displayed correctly under the Details section of your dictionary.
- **Polyphthongs/Blends:** The IPA characters representing the polyphthongs or blends present in your language. Uses the IPA Auto-Fill feature unless it is turned off. Separate each one with a _space_ so they will be displayed correctly under the Details section of your dictionary.
- **Notes:** Any notes about your constructed language's phonology that you want to share. Uses Markdown.
- **Phonotactics**
- **Onset:** What phonological characters can appear at the beginning of a syllable. Separate each with a _comma_.
- **Nucleus:** What phonological characters can appear in the middle of a syllable. Separate each with a _comma_.
- **Coda:** What phonological characters can appear at the end of a syllable. Separate each with a _comma_.
- **Notes:** Any notes about your phonotactical rules laid out above. Uses Markdown.
- **Orthography**
- **Translations:** The specification for how Lexiconga should translate certain character sequences into other character sequences. Use the format "original=new" where "original" is the old letter or sequence of letters and "new" is what you want those letters to change into separated by an equal sign. Put each translation on a _separate line_.
- **Notes:** Any notes about your constructed language's writing system that you want to share. Uses Markdown.
- **Grammar**
- **Notes:** Any notes about your constructed language's grammar that you want to share. Uses Markdown.
#### Settings
- **Prevent Duplicate Words:** Checking this box will prevent the creation of words with the exact same spelling.
- **Words are Case-Sensitive:** Only available when "Prevent Duplicate Words" is checked. Checking this box will allow the creation of words with the exact same spelling if their capitalization is different.
- **Sort by Definition:** Checking this box will sort the words in alphabetical order based on the Definition instead of the Word.
- **Theme:** Set the color theme for the current dictionary.
- **Custom Styling:** Specify custom CSS to change the styling of your dictionary. You can use custom fonts by specifying them here and setting the `font-family` style of the `.orthographic-translation` class!
- **Make Public:** Only visible if logged in with a Lexiconga account. Checking this box will make your dictionary public via a link you can share with others. The link will appear below this checkbox after it is checked.
#### Actions
- **Import JSON:** Choose a previously exported JSON file or older `.dict` file to import. If you are logged in, this will upload the dictionary to your account and display it. If you are not logged in, it will _replace_ your current dictionary with the imported one.
- Again, please note that if you are not logged in, this import process will _**permanently overwrite your current dictionary**_, so please be sure to export your dictionary _before_ you import a new one!
- **Import Words:** Choose a properly-formatted `.csv` file of words to add to your dictionary. This will add all of the words from the file to your current dictionary _even if the list includes duplicate words_. Use the "Download an example file with the correct formatting" link below this field to save a file that you can open with any program that processes CSVs like Microsoft Excel or some other spreadsheet program.
- Please note that when importing words, you must make sure that the parts of speech specified in the parts of speech column are written _exactly as they are in your dictionary settings_ (capitalized, spelled correctly, or any other details). If they are not the same, then you will not be able to use the filters to find the words! So if you import a word with the part of speech set to "adj" or "adjective", but the part of speech in your dictionary's settings is "Adjective", then you will not be able to find the word using the filters!
- If you import a word _without_ a part of speech, you _can_ use the filter's "Blanks" option to find any words with empty parts of speech to help you clean up after the import.
- **Export JSON:** Start a download of a file with your dictionary's name in a `.json` format. _Please note that this may not work as expected on mobile platforms._ This export can be a personal backup for your own uses, to work on multiple dictionaries at a time (i.e. export one dictionary and import the other to work on the one you'd like), or you can share it with friends to view it. The file contains your whole dictionary in a JSON format and is mainly only useful for importing back into Lexiconga.
- **Export Words:** Start a download of all of the words in the currently loaded dictionary into a convenient CSV file format that you can use to re-import into another Lexiconga dictionary or otherwise use as you need it! All of the data is wrapped in double quotes (`"`) to comply with standard CSV format.
- **Delete Dictionary:** Clicking this button will confirm that you want to delete your dictionary. If you confirm, it will permanently destroy your dictionary and all of its data. If you are logged in, this also removes the dictionary from your account.
After making any changes, be sure to click "Save" or "Save & Close" to ensure that your changes are saved!
### Keyboard Shortcuts
**Esc** : Closes any open window (i.e. Dictionary Settings, Settings, this help page, etc.) without saving.
**Ctrl/Control +**
- **Enter/Return:** Submit Word (when typing in Word or Edit Form) _OR_ Save & Close the Settings or Dictionary Settings window.
- **D:** Cycle through dictionary description, details, and stats.
- **E:** Open the Dictionary Settings window.
- **H:** Open this help window.
- **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.
- **Backspace/Delete:** Clear the Search box.
## Accounts
**Note:** Lexiconga is 100% functional _without_ creating an account! Using an account only adds additional syncing features that enable you to store more than one dictionary at a time, access your dictionaries from any computer, and optionally share dicitonaries publicly with a link. _An account is not required_ to build your conlang on your local browser.
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.
### Creating An Account
The first time you create an account, you will need to enter your email address and a password (for logging in) in addition to a "Public Name". Your Public Name will display whenever you log in and will display under your dictionary's name if you make your dictionary public and share it (see below). If you have a dictionary loaded in your browser, it will be automatically uploaded to your account and saved after it is created.
### Logging In
To log in after creating an account, just click the "Log In/Create Account" button and enter your email address and password under the "Log In" section, just like any other account online. You will know that you are logged in from the "Welcome back!" notification at the bottom of the screen when you load the page. You can also know that you're logged in if you see a "Log Out" button instead of "Log In/Create Account" in the top right corner of the screen.
Logging in creates a cookie in your browser that Lexiconga uses to know that you are logged in and verify your account. That means that if your browser prevents cookies, you cannot use a Lexiconga account's features.
### Differences
Every time you save a change to your dictionary's settings or add, edit, or delete a word, the changes are automatically saved to both your browser's localStorage in addition to being sent to your account. Little confirmation messages will appear in the bottom right side of the screen after every change you make to confirm that things are saved correctly.
#### Settings
After logging in, you'll see some additional options in the "Settings" window under new "Account Settings" and "Account Actions" sections.
##### Account Settings
- **Email:** Allows you to specify a different login and contact email address. Make sure that you do not forget what you chose, because there is no way to retrieve your email address if you change it to something you forget!
- **Public Name:** Allows you to change your public name.
- **Allow Emails:** Check this box if you would like to receive emails about important Lexiconga updates. Make sure that you allow emails from addresses at lexicon.ga or check your spam folder just in case. Note that this checkbox does not affect password reset requests—if you forget your password, Lexiconga will send you a password reset email regardless of your choice here.
- **New Password:** Enter a new password in this field _only if you want to change it_! Leave this field blank to prevent your password from changing.
If you change any of the options above, be sure you click the "Save" or "Save & Close" button.
##### Account Actions
- **Change Dictionary:** Shows all dictionaries created on your account. Choose a different one from the drop-down box to instantly load and display that dictionary.
- **Create New Dictionary:** Clicking this button will create a new dictionary on your account and display the new empty dictionary to you.
#### Public Dictionaries
When a dictionary is marked as public, you can share its public link and allow anyone to view and search its contents without being able to make changes. Public dictionaries also have the ability to share specific word entries using the "➦" buttons in each word box. When viewing a word, anyone can still read the dictionary's description and details.
To log in or create an account when viewing a dictionary, you need to go back to the main Lexiconga page. You can get there by clicking the Lexiconga logo.
### Forgot Your Password?
If you forget your password, you can request a password reset email by clicking the "Forgot Password" button in the "Log In/Create Account" window, entering the email address associated with your account, and clicking "Email Password Reset Key". This will send an email (_check your spam_) with a link that will allow you to reset your password. When you go to the link provided, you'll be able to enter a new password that you can log in with.
### Lockout
If you manage to enter your password wrong 10 times, you'll be locked out from logging in for 1 hour. Use this time to try to remember your password or something. You can get an idea of how long you've waited by trying to log in again. After an hour has passed, refresh the page again and you'll get another 10 tries.
## Problems or Requests
Please report any problems you come across to the [Lexiconga Issues page](https://github.com/Alamantus/Lexiconga/issues). You can also submit enhancement requests to the same place if you have any requests for new features.
## Update Log
You can see all previous updates to Lexiconga on the [Lexiconga Releases page](https://github.com/Alamantus/Lexiconga/releases).
## Open Source
Lexiconga's source code is fully open and readable on Github here: https://github.com/Alamantus/Lexiconga
## Thanks!
If you like Lexiconga and want to help contribute to keeping it online and motivate me to keep adding new features, you can use **[Buy Me A Coffee](https://buymeacoff.ee/robbieantenesse)** to give a one-time donation or **[Liberapay](https://liberapay.com/robbieantenesse)** to make a recurring donation.
I hope you enjoy Lexiconga and that it helps you build some awesome languages.
Robbie Antenesse

Some files were not shown because too many files have changed in this diff Show More