I personally find it helpful to see what aspects of a program I already implemented and what features I need to add. Just for fun, I decided to write up a program to do that. This is the first program for which I tried to write clean looking code. Sorry for the long, overcomplicated code.
The way the program stores/retrieves data is weird and inefficient mainly because I don't know a better way.
The program is html and javascript communicating to a Python webserver. I also hosted the program on the webserver to make cross origin stuff easier to deal with. Unfortunately all the filenames are hardcoded into the program.
webserver.py:
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
import SocketServer
startData = "###DATAGOESHERE###"
endData = "###ENDDATAGOESHERE###"
def readFile(fileName):
with open(fileName,'r') as f:
data = f.read()
f.close()
return data
def replaceWithData(string, data):
data = data.strip()
if data.strip() == "":
return string.replace(startData, "").replace(endData, "")
else:
start = string.find(startData)
end = string.find(endData)+len(endData)
string = string[:start]+data+string[end:]
return string
def writeFile(fileName, data):
with open(fileName,'w') as f:
f.write(data)
f.close()
def serveStaticApp(request, url):
data = readFile('data.html')
html = readFile('main.html')
html = replaceWithData(html, data)
request.wfile.write(html)
def serverStaticFile(request, url):
data = readFile(url)
request.wfile.write(data)
def saveData(request, url):
length = int(request.headers.getheader('content-length'))
data = request.rfile.read(length).strip()
writeFile('data.html', data)
funcMap = {
"app": serveStaticApp,
"main.js":serverStaticFile,
"save": saveData,
"main.css": serverStaticFile,
}
def handleRequest(request):
request._set_headers()
for i in funcMap.keys():
if request.path[-len(i):] == i:
funcMap[i](request, i)
return
request.wfile.write("<html><body><h1>REQUEST!</h1></body></html>")
class S(BaseHTTPRequestHandler):
def _set_headers(self):
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
def do_GET(self):
handleRequest(self)
def do_HEAD(self):
self._set_headers()
def do_POST(self):
handleRequest(self)
def run(server_class=HTTPServer, handler_class=S, port=8080):
server_address = ('localhost', port)
httpd = server_class(server_address, handler_class)
print 'Starting...'
httpd.serve_forever()
if __name__ == "__main__":
from sys import argv
if len(argv) == 2:
run(port=int(argv[1]))
else:
run()
main.html
<!DOCTYPE>
<html>
<head>
<link rel="stylesheet" href="main.css" type="text/css">
</head>
<body>
<div>
<span id="not-implemented-label">Not Implemented:</span>
<span id="implemented-label">Implemented:</span>
</div>
<span id="save">
###DATAGOESHERE###
<select id="not-implemented-select" size="2" multiple="true"></select>
<span id="controls">
<input type="button" value="<-" id="left-arrow-button">
<input type="button" value="LOAD" id="load-button">
<input type="button" value="->" id="right-arrow-button"><br>
<input type="button" value="DELETE" id="delete-button">
</span>
<select id="implemented-select" size="2" multiple="true"></select>
###ENDDATAGOESHERE###
</span>
<br>
<br>
<input type="text" id="title-input" placeholder="Title"><br>
<textarea id="description-input" placeholder="Description"></textarea>
<span id="save-controls">
<input type="button" value="SAVE OVER" id="save-over-button"><br>
<input type="button" value="SAVE NEW" id="save-new-button"><br>
<input type="button" value="CLEAR" id="clear-button">
</span>
<script src="functions.js"></script>
<script src="main.js"></script>
</body>
</html>
main.css
select{
width:40vw;
height:60vh;
}
textarea{
width:80vw;
height:30vh;
}
#title-input{
width:80vw;
}
#controls{
display:inline-block;
width:8em;
text-align: center;
}
#save-controls{
display:inline-block;
}
#not-implemented-label{
display:inline-block;
width:calc(40vw + 8em);
}
.description{
display:none;
}
main.js
"use strict";
var mostRecentlySelectedOptions = [];
var allowTextOverrride = true;
var saveServerURL = "/save"
var cachedElements = {};
function gebi(id){
var cachedElement = cachedElements[id];
if(cachedElement === undefined){
cachedElement = document.getElementById(id);
cachedElements[id] = cachedElement;
return cachedElement;
}else{
return cachedElement;
}
}
function slice(collection){
return [].slice.call(collection);
}
function moveOptions(options, toSelect){
options = options.slice();
for(var i = 0; i < options.length; i++){
toSelect.appendChild(options[i]);
}
saveToServer();
}
function setInputs(title, description){
allowTextOverrride = true;
gebi("title-input").value = title;
gebi("description-input").value = description;
}
function getInputs(){
return {
"title": gebi("title-input").value,
"description": gebi("description-input").value
};
}
function getSafeInputs(){
var inputs = getInputs();
if(inputs.title.trim() == ""){
inputs.title = "<untitled>";
}
inputs.title = inputs.title.trim();
inputs.description = inputs.description.trim();
return inputs;
}
function clearInputs(){
setInputs("", "");
}
function moveSelectedOptions(toSelect){
moveOptions(mostRecentlySelectedOptions, toSelect);
}
function onSelectionChange(target){
mostRecentlySelectedOptions = slice(target.selectedOptions);
if(allowTextOverrride){
loadSelectedOption();
}
}
function deselectAllInSelect(selectElement){
var selected = selectElement.selectedOptions;
for(var i = 0; i < selected.length; i++){
selected[i].selected = false;
}
}
function saveText(){
allowTextOverrride = true;
saveToServer();
}
function saveToServer(){
var dataToSave = gebi("save").innerHTML;
var xhttp = new XMLHttpRequest();
xhttp.open("POST", saveServerURL, true);
xhttp.setRequestHeader("Content-type", "text/plain");
xhttp.send(dataToSave);
}
function removeOptions(options){
options = options.slice();
for(var i = 0; i < options.length; i++){
options[i].parentNode.removeChild(options[i]);
}
saveToServer();
}
function removeSelectedOptions(){
removeOptions(mostRecentlySelectedOptions);
}
function setOptionData(option, title, description){
option.text = title;
option.setAttribute("data-description", description);
}
function getOptionData(option){
return {
"title": option.text,
"description": option.getAttribute("data-description")
};
}
function createOption(title, description){
var option = document.createElement("option");
setOptionData(option, title, description);
return option;
}
function addOption(title, description, parentSelect){
var option = createOption(title, description);
parentSelect.add(option);
deselectAllInSelect(parentSelect);
option.selected = true;
}
function saveNew(){
var inputData = getSafeInputs();
var parentSelect = gebi("not-implemented-select");
addOption(inputData.title, inputData.description, parentSelect);
saveText();
}
function loadSelectedOption(){
if(mostRecentlySelectedOptions.length == 1){
var selectedOption = mostRecentlySelectedOptions[0];
var data = getOptionData(selectedOption);
setInputs(data.title, data.description);
}
}
function saveOverwrite(){
if(mostRecentlySelectedOptions.length == 1){
var selectedOption = mostRecentlySelectedOptions[0];
var inputData = getSafeInputs();
setOptionData(selectedOption, inputData.title, inputData.description);
saveText();
}else{
saveNew();
}
}
function addEventListeners(){
var notImplementedSelect = gebi("not-implemented-select");
var implementedSelect = gebi("implemented-select");
gebi("right-arrow-button").addEventListener("click", function(){
moveSelectedOptions(implementedSelect);
});
gebi("left-arrow-button").addEventListener("click", function(){
moveSelectedOptions(notImplementedSelect);
});
gebi("load-button").addEventListener("click", function(){
loadSelectedOption();
allowTextOverrride = false;
});
gebi("delete-button").addEventListener("click", function(){
removeSelectedOptions();
});
gebi("save-over-button").addEventListener("click", function(){
saveOverwrite();
});
gebi("save-new-button").addEventListener("click", function(){
saveNew();
});
gebi("clear-button").addEventListener("click", function(){
clearInputs();
});
implementedSelect.addEventListener("change", function(event){
onSelectionChange(event.target);
});
implementedSelect.addEventListener("focus", function(event){
onSelectionChange(event.target);
});
notImplementedSelect.addEventListener("change", function(event){
onSelectionChange(event.target);
});
notImplementedSelect.addEventListener("focus", function(event){
onSelectionChange(event.target);
});
gebi("title-input").addEventListener("keypress", function(){
allowTextOverrride = false;
});
gebi("description-input").addEventListener("keypress", function(){
allowTextOverrride = false;
});
window.addEventListener("beforeunload", function(){
//alert();
});
}
addEventListeners();
To run it, put all the files into the same directory and run the Python file. You also need a blank file named data.html
in the same directory. Then visit http://localhost:8080/app
to access the app.
You can put a small title for the task and a longer description of the task and click SAVE NEW. SAVE OVER will save over whatever one you have selected. The two arrow buttons move the task between columns. It is important to note that the webserver should not be run over the internet.
I'm particularly interesting in the following:
- Are my variables/functions well named?
- Are my functions doing too much or too little?
- Are there cases that you can get unexpected behavior?
- Do you feel that the code is easily extendable/maintainable?
- What can I do about the long list of
addEventListener
s?