Frontend
Pages
Overview
simpliPFy is a single page application. This is due to the loading times of Pyodide. Pyodide is used in for the calculations and simplifications. Pyodide is a Python interpreter ported to Webassembly. It is easiest to avoid reloading Pyodide by not changing the webpage while navigating. For intuitivity the simplipfy.org appears to have pages. This is realised by containers in the index.html file.
<!--####################################################################################################
#################################### Landing page #########################################
#####################################################################################################-->
<div class="container-fluid m-0 p-0 text-center justify-content-center" id="landing-page-container"></div>
In the comment the page name is noted to structure the index.html. The container beneath represents a page.
To show a page the style attribute is set to block. To hide a page the style attribute is set to none.
The logic of the pages is encapsulated in the Pages class. A Page needs to do the following basic things:
show
hide
update Language
update Color ( change from dark to light mode or vise versa)
A Page displays some content. The content on a Page has some functions in common with the Pages, like
changing color and language. Therefore the Pages and Content share the same base class FunctionsInterface.
In JavaScript there are no Interfaces but the class shall be treated as one. It does not make sense to create
instances of the FunctionsInterface class. But it “asserts” that each child has common functions that can be called.
(If you update the picture below make sure it has as viewbox attribute to assert the whole picture is shown)
A Page is made up of Content. Because Content and Page share the same base functions like updateLang() and
updateColor() a Page does not need to know how the Content it displays behaves on changes it simply calls the
implementation of the Content. This asserts consistency throughout the code if updateLang() on a Page
is called that means updateLang() is called on each Content the Page displays. The FunctionsInterface
may be extended if there are actions that make sense on a Page and the Content it displays. The diagram
may be outdated if you need the functions on each class see the API-Documentation.
Implement new Page
At .../Inskale/Pyodide/src/pages/template are templates for:
page implementation
content implementation
modal implementation
Implement the specified functions, add new ones if necessary or usefull for the implementation of the Page.
Always see the base implementation maybe this does all you need. Always call the base implementation to assert
class internal values are set i necessary.
Setup
use the super.beforeSetup() and the super.afterSetup as shown in the template:
setup() {
if (!super.beforeSetup()) return;
//class specific setup of content
super.afterSetup();
}
setup adds the html code element of the page to the index.html super.beforeSetup asserts:
that the setup wasn’t already started
the page div is hidden.
super.afterInit asserts:
that the eventlisteners are added to the page (only possible after the setup)
that the internal values of the class represent that the class was setup
Initialize
use the super.beforeInit() and the super.afterInit as shown in the template:
async initialize() {
if (!super.beforeInit()) return awaitVal(() => this.isInitialized, () => {}); //this could create an endless wait if a class is not setup and the init is not called again
// class specific init of content
super.afterInit();
}
initialize generates or updates elements that wait for other values, files or data and therefore is async. If called twice returns a promise which resolves when the page is initialized and therefore ready to be displayed.
super.beforeInit asserts:
that the init wasn’t already started
the page is setup (if not calls the setup with a warning)
super.afterInit asserts:
that the internal values of the class represent that the class was initialized
updates the color of the page
updates the language of the page
Language Management
Structure
The language files and Language Manager is under .../Inskale/Pyodide/src/scripts/languages. Each language gets a
Folder with the official language abbreviation. A list of those abbreviations can be found on Wikipedia.
Ideally each page gets its own language file. Due to legacy code this is not always the case.
All files in the language folder are concatenated into one file e.g. for english lang.en.js.
This shortens loading times.
Adding a new language
If you add a new Language you have to add its abbreviation to .../Inskale/Pyodide/src/scripts/definitions/languageSymbols.js.
This is necessary because not all languages are loaded at startup. English is always loaded any other language is
loaded on demand.
The simplest way to add a new Language is to copy an existing language and rename the folder and files.
In the .../Inskale/Pyodide/src/scripts/languages/languageManager.js you habe to adjust:
setLang(lang)
setBrowserLang()
And add:
setLang<lang abbreviation>() [e.g. setLangEn()]
Adjust the layout .../Inskale/Pyodide/index.html at the div with the id <div id="lang-dropdown" class="dropdown">.
Adjust this <li> element:
<li>
<a id="select-english" class="d-flex lang-selector" style="text-decoration: none; color:white">
<img src="src/resources/navigation/uk.png" class="flag my-auto mx-2" alt="englishFlag">
<p class="my-auto" style="font-size: 20px">English</p>
</a>
</li>
Adjust the following things:
change the id from
select-englishto select-<newLanguageName>add an flag image to
.../Inskale/Pyodide/src/resources/navigationand adjust the img src linkchange
Englishin the <p> tag to the new language name
And copy it into the <ul> tag under the last <li> element in index.html.
Adjust the dropdown in the Navigation sidebar at .../Inskale/Pyodide/src/pages/navigation/sideBar.js
add class member:
this.select<newLangName> = document.getElementById("select-<newLanguageName>");this references the <li> element created earlier and added to theindex.htmlso the ids have to match. The class member is for internal use to avoid having to rewritedocument.getElement...quite some times.add a event listener for the added <li> element
this.select<newLangName>.addEventListener("click",async () => { await languageManager.setLang<newLangAbbreviation>(); pageManager.updateLang(); })
How it works
Each text you see on simplipfy.org is loaded from a language file. Those language files consist of Objects like this:
landingEnTexts = {
startBtn:
"START",
landingPageGreeting:
"a free browser tool for learning<br>" +
"how to simplify electrical circuits",
keyFeature1heading:
"Understanding",
...
}
those Objects always include the language abbreviation to distinguish between languages and avoid variable clashing in the global scope.
The language objects are all combined in lang.<language abbreviation>.js:
window.english = {
landingPage: landingEnTexts,
selector: selectorEnTexts,
alerts: alertsEnTexts,
...
}
Each file must have the same structure otherwise the language switching won’t work.
The LanguageManager class
The LanguageManager class is in .../Inskale/Pyodide/src/scripts/languages/languageManager.js. It can be used in the
project to retrieve language strings from the language files. It always returns the correct string for the currently set
language. Each page and content uses the language manager to get the current language strings on the page setup and on
language updates that are triggered by a language change. To get a language string use:
languageManager.currentLang...
this is a getter and returns a proxy. This way each call to a language is guarded and if it fails because a string is
not defined it automatically falls back to english. Therefore always implement new strings in english then in other languages.
If the string also is not in the english lang files it will warn in the console and return ! undefined ! as a string.
Storage Management
The local storge is managed by the LocalStorageManager. The LocalStorageManager
has members that are derived from LocalStorageWriter treat LocalStorageWriter as an abstract class.
Each value saved to the local storage shall be stored with a class derived from LocalStorageWriter this way
misspelling and creating duplicates is avoided and logic to retrieve and save data is moved to a fix place.
LocalStorageWriter
Supports writing and reading values from and to the local storage of the browser
Save a new Value or Object
class SomeValueToSave extends LocalStorageWriter{
constructor() {
super("<keyName e.g. ClassName>");
}
save(){
super.set(pageName);
}
load() {
let [success, pageName] = super.get()
if (!success) {
this.save("newLandingPage");
}
return [true, pageName]
}
}
Each class should support save and load functions.
load() returns the object or value loaded
save() takes in the object or value converts it to a json string if it is a object or a plain string otherwise
constructor() defines the key used in the local storage
Each object derived from LocalStorageWriter has the _get, _set and _delete methods treat them as private.
Those directly interact with the local storage using them would defeat the purpose of the extended classes.
.bundleLast Files
Those files mark directories where all files are combined into a <folderName>.bundle.js file including subdirectories.
This is done when .../Inskale/Pyodide/Scripts/buildBundles.py is executed. The script is integrated in the build
process. Combining multiple files into one bundle reduces loading time because it removes loading overhead for multiple
small files.
The file named in .bundleLast is appended to the bundle last to avoid declaration errors if js files rely on each other.
You can also creat a specific order by adding all files to the .bundleLast file in the order you want them to be
append to the bundle file. The bundle files are only visible in .../Inskale/Pyodide/dist because they are moved
in the build process. If you see them in your project your build process certainly failed. Having the bundles in the
project structure hurts the idea because of duplicate code and references if you configure your ide exclude
.../Inskale/Pyodide/dist from your project files.
Definitions
To avoid cluttering the window with variables the definitions used in this project are defined in files at
.../Inskale/Pyodide/src/scripts/definitions and appended to an object called definitions shown with the example
of the allowed directory names of a circuit file:
/** @type {{allowedDirNames: Object<string,string>}} */
window.definitions = window.definitions || {};
window.definitions.allowedDirNames = {
quickstart: "quickstart",
resistor: "resistor",
symbolic: "symbolic",
capacitor: "capacitor",
inductor: "inductor",
mixed: "mixed",
kirchhoff: "kirchhoff",
wheatstone: "wheatstone",
magnetic: "magnetic"
}
With the exception of the class ColorDefinitions.
Matomo
Matomo tracking
This website is using matomo to track user actions. The focus is set on respecting the user privacy which means some things can not be tracked, for example returning users. However, specific events can still be analyzed, helping to see how the users interact with simplipfy.
Events
The following events are tracked manually by the frontend:
Circuit Events (selected circuit, circuit finished/aborted, …)
Error Events (failure to load, …)
Configuration Events (dark mode, language)
If you want to adopt some of the functionality, you can use the matomoHelper.js file in the src/scripts/utils directory.
It specifies different actions like
const circuitActions = {
Finished: "Fertig",
Aborted: "Abgebrochen",
Reset: "Reset",
ErrCanNotSimpl: "Kann nicht vereinfacht werden",
ViewVcExplanation: "VC Rechnung angeschaut",
ViewZExplanation: "Z Rechnung angeschaut",
ViewTotalExplanation: "Gesamtrechnung angeschaut",
ViewSolutions: "Lösungen angeschaut",
}
The events are currently sent on german, the names themselves don’t need to be exactly this, however it is a good idea to have a consistent naming scheme, meaning you should not change the names because then the analysis is split between different names for the same event.
Usage
Circuit Event
Use the pushCircuitEventMatomo(action, value=-1) function to send a circuit event to Matomo.
Action can be one of the defined circuitActions.
Error Event
Use the pushErrorEventMatomo(action, error) function to send an error event to Matomo,
where action is a defined errorActions and error is the thrown error or an error string.
Configuration Event
Use the pushConfigurationEventMatomo(action, configuration, value=-1) function to send a configuration event to Matomo,
where action is one of the defined configActions and value is the value of the configuration (e.g. language or dark mode).
MathJax
Notes for Mathjax usage
Since MathJax.typeset() is asynchronous and use in various files, a better solution is to use await MathJax.typesetPromise() which returns a promise that resolves when the typesetting is complete. With this it should not be possible to have two MathJax renders at the same time which could result in errors.
Pyodide Worker
To get a more fluid rendering in the frontend, pyodide is moved to a webworker. In this worker, all functions for simplipfy are called.
Workflow
The handling of pyodide is split into 3 parts: - A webworker that handles the pyodide instance (pyodideWorker.js) - A webworker API that is called from the frontend classed to access the pyodide instance (pyodideWorkerAPI.js) - A specific group of functions inside an object that are grouped as one API, example
pyodideAPI.js
stepSolverAPI.js
state.apis.kirchhoffSolverjs
The splitting of the functions into different files is done to keep the code clean and organized. The calls inside this files are exactly the same. Following is an example of how the API is used.
Usage
At some place the function exampleAPICall (a python function in the backend) should be called. For this, a exampleAPI.js is created, looking something like this
class ExampleAPI {
constructor(worker) {
this.worker = worker; // the worker instance
}
exampleAPICall(a, b) {
return requestResponse(this.worker, {
action: "exampleAPICall",
data: { a: a, b: b },
});
}
The functions call `requestResponse` which is a function that handles the communication with the worker.
It returns a promise that resolves when the worker has finished the calculation and returns the result.
For this, the resolve return values have to be handled, this happens in `getResolve` in pyodideWorkerAPI.js:
function getResolve(msg, resolve, event) {
try {
if (msg.action === "someAction") {
resolve(event.data.status);
} else if (msg.action === "exampleAPICall") {
resolve([event.data.c, event.data.d]);
} ...
And for `event.data.c` and `event.data.d` to be available, the pyodideWorker.js has to be adapted, e.g. like this:
try {
// Make sure pyodide and micropip is loaded before doing anything else
self.pyodide = await self.pyodideReadyPromise;
// ###################### Some API ######################
if (event.data.action === "exampleAPICall") {
await self.pyodide.someImportedModule.exampleAPICall(event.data.data.a, event.data.data.b);
self.postMessage({id: _id});
}
...
Important things to note
The strings like “exampleAPICall” must match between the different files (improvable)
Each function in the pyodideWorker.js must finish with a self.postMessage({id: _id}) to send the result back to the frontend.
The calls of the worker are filtered by this id, so that the right result is sent back to the right request.
Progress Bar
This tutorial will show you how to use the custom progress bar that show the progress for pyodide loading and will be enabled when pyodide loading is finished.
See allStyles.css for the style with .progress-stripes and @keyframes moveStripes
How to use
To make a button a progress bar, you have to add the class circuitStartBtn to the class list. You also need to add a few layers inside the button. An example of a button, that will be a progress bar and is enabled when loading is done is:
<button id="someId" class="circuitStartBtn btn btn-warning text-dark px-5">
<div class="fill-layer"></div>
<div class="progress-stripes"></div>
<span class="button-text">start/or your text</span>
</button>
As mentioned before, this does not have to be a button, it can also be a div, circuitStartBtn is a custom class that will add the necessary styles to the element if the additional layers are inside.
If you want to create a button that contains the progress bar, you have to use the <button> tag like in the example above to be able to disable the button at the beginning, you can not use a div with the bootstrap btn class. After all you want the button to be enabled only when pyodide is loaded. Disable the button after creating it
let btn = document.getElementById("someId");
btn.disabled = true;
If you only want a progress bar that is not a button but just shows the pyodide waiting progress, you can make it a <div> with the class circuitStartBtn and the additional layers inside and don’t have to disable it.
When loading is done, the function
selectorBuilder.enableStartBtns()
is called to set disabled to false and enables all the circuit start buttons
and
finishStartBtns()
is called to do some final touches on all progress bars and circuit start buttons. You don’t have to adjust the functions if you use a progress bar like the example above.
Session Tracking
This page should explain how the session tracking via QR Codes is implemented and what the use case is.
Work flow
The idea behind session tracking is the following use case: - a teacher creates a QR code from a circuit file, the tracking id with name is cached in the local storage - the QR code can be shared with students - when the teacher navigates to the QR Track Viewer and selects this tracking id, all live events are shown in a table - only the live events will be shown there, not any events that happened before this time - when the teacher leaves the website, all events for this session are deleted from the server database
Implementation
On the server, a php script src/session.php is used to handle the database requests.
The script allowes the following actions:
- addSessionId: Adds a session id to the list of valid session ids
- deleteSessionId: Deletes a session id from the list of valid session ids
- send: Posts an event to the database with a session id
- read: Reads all events for a session id from the database
Only when a session id is added to the valid list of session ids, it can be used to post events to the database. When a session id is deleted, all events for this session id are deleted from the database.
Frontend API
You can view the Frontend API docs here: Frontend API