Browse Source

Implement a file menu and group the file operations there

better-rectangles
Tissevert 6 months ago
parent
commit
fbc64dc0d8
26 changed files with 460 additions and 195 deletions
  1. +1
    -0
      .gitignore
  2. +12
    -5
      Makefile
  3. +27
    -0
      css/main.css
  4. +7
    -43
      css/page.css
  5. +18
    -0
      css/page/selector.css
  6. +19
    -0
      css/toolbar.css
  7. +39
    -0
      css/toolbar/menu.css
  8. +11
    -7
      index.html
  9. +33
    -0
      js/File.js
  10. +23
    -3
      js/GUI.js
  11. +0
    -0
      js/Geometry/Rectangle.js
  12. +0
    -0
      js/Geometry/Segment.js
  13. +2
    -0
      js/Main.js
  14. +0
    -0
      js/Quality.js
  15. +38
    -0
      js/Scoria.js
  16. +0
    -0
      js/Toolbar/EditMode.js
  17. +42
    -0
      js/Toolbar/Menu.js
  18. +80
    -0
      js/Toolbar/Page.js
  19. +65
    -0
      js/View.js
  20. +7
    -1
      js/XML.js
  21. +29
    -8
      js/XML/ALTO.js
  22. +7
    -0
      js/XML/ALTO/Element.js
  23. +0
    -13
      src/File.js
  24. +0
    -36
      src/Scoria.js
  25. +0
    -48
      src/Toolbar/Page.js
  26. +0
    -31
      src/View.js

+ 1
- 0
.gitignore View File

@@ -1 +1,2 @@
main.js
style.css

+ 12
- 5
Makefile View File

@@ -1,12 +1,19 @@
SRCDIR=src
SRC=$(shell find $(SRCDIR) -type f -name '*.js')
JS_SRCDIR=js
CSS_SRCDIR=css
JS_SRC=$(shell find $(JS_SRCDIR) -type f -name '*.js')
CSS_SRC=$(shell find $(CSS_SRCDIR) -type f -name '*.css')
LIB=unitJS
TARGET=main.js
JS=main.js
CSS=style.css
TARGET=$(JS) $(CSS)

all: $(TARGET)

$(TARGET): $(SRC)
sjw $(LIB:%=-I %) -o $@ $(SRCDIR)
$(JS): $(JS_SRC)
sjw $(LIB:%=-I %) -o $@ $(JS_SRCDIR)

$(CSS): $(CSS_SRC)
cat $^ > $@

mrproper:
rm $(TARGET)

+ 27
- 0
css/main.css View File

@@ -0,0 +1,27 @@
html, body {
margin: 0;
}

#workzone {
position: fixed;
overflow: scroll;
z-index: -1;
left: 0;
top: 3em;
right: 0;
bottom: 0;
}

#windowCatchAll {
display: none;
position: absolute;
z-index: 1;
left: 0;
top: 0;
right: 0;
bottom: 0;
}

#windowCatchAll.on {
display: block;
}

gui.css → css/page.css View File

@@ -1,25 +1,3 @@
html, body {
margin: 0;
}

#toolbar {
background: #ddd;
border-top: 1px solid #eee;
border-bottom: 1px solid #ccc;
}

#fileNumber {
width: 4em;
}

#wcThreshold {
width: 3.5em;
}

#saveScoria {
cursor: pointer;
}

#page {
padding: 1em;
background: #fffdf5;
@@ -30,35 +8,21 @@ html, body {
transform: translateX(-50%);
}

#selector {
display: none;
position: fixed;
border: 1px dashed #aaa;
}

#selector.on {
display: block;
#page * {
position: absolute;
}

#page.editMode .String {
cursor: crosshair;
}

#page.editMode .String.deleted {
cursor: not-allowed;
}

#selector, #page.freeze {
cursor: crosshair;
user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
#page.editMode .String:hover {
border: 1px dashed red;
margin: -1px 0 0 -1px;
}

#page * {
position: absolute;
#page.editMode .String.deleted {
cursor: not-allowed;
}

#page p {

+ 18
- 0
css/page/selector.css View File

@@ -0,0 +1,18 @@
#selector {
display: none;
position: fixed;
border: 1px dashed #aaa;
}

#selector.on {
display: block;
}

#selector, #page.freeze {
cursor: crosshair;
user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
}

+ 19
- 0
css/toolbar.css View File

@@ -0,0 +1,19 @@
#toolbar {
background: #ddd;
border-top: 1px solid #eee;
border-bottom: 1px solid #ccc;
user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
height: 3em;
}

#fileNumber {
width: 4em;
}

#wcThreshold {
width: 3.5em;
}

+ 39
- 0
css/toolbar/menu.css View File

@@ -0,0 +1,39 @@
.menu {
cursor: default;
}

.menu > * {
z-index: 2;
}

.menu span {
position: relative; /* needed for z-index to work */
}

.menu ul {
display: none;
list-style: none;
padding: 0;
margin: 0;
position: absolute;
background: #ddd;
border: 1px solid #ccc;
}

.menu.open ul {
display: block;
}

.menu ul li {
padding: 0.2em 0.5em;
}

.menu.open span, .menu ul li:hover {
background: #777;
color: #fff;
}

.menu ul li.disabled {
color: #888;
background: #ddd;
}

+ 11
- 7
index.html View File

@@ -4,13 +4,19 @@
<meta charset="utf-8">
<title>Chaoui</title>
<script src="main.js"></script>
<link rel="stylesheet" href="gui.css" type="text/css"/>
<link rel="stylesheet" href="style.css" type="text/css"/>
</head>
<body>
<div id="toolbar">
<span>
<label for="inputFiles">Charger des fichiers ALTO</label><input id="inputFiles" type="file" name="inputFiles" accept="text/xml,.xml" multiple/>
</span>
<div id="file" class="menu">
<span>Fichier</span>
<ul>
<li id="loadALTO">Charger des fichiers ALTO</li>
<li id="saveALTO" class="disabled">Exporter la page courante modifiée</li>
<li id="loadScoria">Importer des scories</li>
<li id="saveScoria">Exporter les scories</li>
</ul>
</div>
<span>
<label for="fileNumber">#</label><input id="fileNumber" type="number" name="fileNumber" min="0" max="0" step="1" value="0"/>
<span id="fileName"></span>
@@ -21,13 +27,11 @@
<span>
<input id="editMode" type="checkbox" name="editMode"/><label for="editMode">Éditer</label>
</span>
<span>
<a download="scories.csv" id="saveScoria" title="Exporter la liste des suppressions">💾</a>
</span>
</div>
<div id="workzone">
<div id="page"></div>
<div id="selector"></div>
</div>
<div id="windowCatchAll"></div>
</body>
</html>

+ 33
- 0
js/File.js View File

@@ -0,0 +1,33 @@
import * as Dom from UnitJS.Dom;

return {
load: load,
pick: pick,
save: save
};

function pick(attributes) {
return function(f) {
attributes.type = 'file';
var input = Dom.make('input', attributes);
input.addEventListener('change', function() {
f(input);
});
input.click();
}
}

function load(file) {
return function(f) {
var fileReader = new FileReader();
fileReader.addEventListener('load', function() {
f(fileReader.result);
})
fileReader.readAsText(file);
};
}

function save(data, name) {
var a = Dom.make('a', {download: name, href: data});
a.click();
}

src/GUI.js → js/GUI.js View File

@@ -1,11 +1,23 @@
import printSpace from XML.ALTO.Element;

/* Display */
var windowCatchAll = document.getElementById('windowCatchAll');
var workzone = document.getElementById('workzone');
var page = document.getElementById('page');
page.style.width = printSpace.pxWidth + 'px';
var selector = document.getElementById('selector');

var inputFiles = document.getElementById('inputFiles');
/** Toolbar **/
/* Menus */
var menus = document.getElementsByClassName('menu');
var fileMenu = document.getElementById('file');
var loadALTO = document.getElementById('loadALTO');
var saveALTO = document.getElementById('saveALTO');
var loadScoria = document.getElementById('loadScoria');
var saveScoria = document.getElementById('saveScoria');

/* Controls */
//var inputFiles = document.getElementById('inputFiles');
var fileNumber = document.getElementById('fileNumber');
var fileName = document.getElementById('fileName');
var wcThreshold = document.getElementById('wcThreshold');
@@ -14,12 +26,20 @@ var saveScoria = document.getElementById('saveScoria');

return {
editMode: editMode,
fileMenu: fileMenu,
fileName: fileName,
fileNumber: fileNumber,
inputFiles: inputFiles,
page: page,

loadALTO: loadALTO,
saveALTO: saveALTO,
loadScoria: loadScoria,
saveScoria: saveScoria,

//inputFiles: inputFiles,
menus: menus,
page: page,
selector: selector,
wcThreshold: wcThreshold,
windowCatchAll: windowCatchAll,
workzone: workzone
};

src/Geometry/Rectangle.js → js/Geometry/Rectangle.js View File


src/Geometry/Segment.js → js/Geometry/Segment.js View File


src/Main.js → js/Main.js View File

@@ -1,5 +1,7 @@
import * as EditMode from Toolbar.EditMode;
import * as Menu from Toolbar.Menu;
import * as Page from Toolbar.Page;

EditMode.sync();
Page.syncNumber();
Menu.init();

src/Quality.js → js/Quality.js View File


+ 38
- 0
js/Scoria.js View File

@@ -0,0 +1,38 @@
import File;

var scoriae = {};

return {
getScoriae: getScoriae,
isScoria: isScoria,
setScoria: setScoria,
toggleWord: toggleWord
};

function getScoriae() {
var result = [];
for(var id in scoriae) {
result.push(id);
}
return result;
}

function isScoria(wordId) {
return scoriae[wordId] || false;
}

function setScoria(wordId, state) {
if(state == undefined || state) {
scoriae[wordId] = true;
} else {
delete scoriae[wordId];
}
}

function toggleWord(domElem, forceState) {
if(editMode.checked) {
var state = forceState != undefined ? forceState : !scoriae[domElem.id];
setScoria(domElem.id, state);
domElem.classList.toggle('deleted', state);
}
}

src/Toolbar/EditMode.js → js/Toolbar/EditMode.js View File


+ 42
- 0
js/Toolbar/Menu.js View File

@@ -0,0 +1,42 @@
import {menus, windowCatchAll} from GUI;

var selectedMenu = null;

window.addEventListener('keydown', function(e) {
switch(e.keyCode) {
case 0x1b: closeMenu(); break;
}
});
windowCatchAll.addEventListener('click', closeMenu);

return {
init: init
}

function init() {
for(var i = 0; i < menus.length; i++) {
menus[i].querySelector('span').addEventListener('click', toggleMenu(menus[i]));
menus[i].querySelector('ul').addEventListener('click', closeMenu);
}
}

function toggleMenu(menu) {
return function() {
if(selectedMenu == undefined) {
menu.classList.add('open');
selectedMenu = menu;
windowCatchAll.className = 'on';
} else {
closeMenu();
}
};
}

function closeMenu(e) {
if(selectedMenu != undefined
&& !(e != undefined && e.target.classList.contains('disabled'))) {
selectedMenu.classList.remove('open');
selectedMenu = null;
windowCatchAll.className = '';
}
}

+ 80
- 0
js/Toolbar/Page.js View File

@@ -0,0 +1,80 @@
import File;
import * as GUI from GUI;
import * as Async from UnitJS.Async;
import XML;
import * as ALTO from XML.ALTO;
import View;

var files = null;

GUI.loadALTO.addEventListener('click', function() {
Async.run(
Async.bind(
File.pick({accept:"text/xml,.xml", multiple: true}),
function(input) {
files = input.files;
GUI.fileNumber.value = 0;
syncNumber();
updateNumber();
GUI.saveALTO.classList.remove('disabled');
return Async.wrap();
}
)
);
});

GUI.saveALTO.addEventListener('click', function() {
if(!GUI.saveALTO.classList.contains('disabled')) {
var file = files[GUI.fileNumber.value];
Async.run(
Async.bind(
File.load(file),
Async.map(XML.parse),
Async.map(function(ALTOFile) {
var payload = encodeURIComponent(XML.serialize(ALTO.edit(ALTOFile)));
File.save('data:text/xml,' + payload, file.name);
})
)
);
}
});

GUI.fileNumber.addEventListener('change', updateNumber);
window.addEventListener('keydown', function(e) {
switch(e.keyCode) {
case 0x25: moveFile(-1); break;
case 0x27: moveFile(+1); break;
}
});

return {
syncNumber: syncNumber
}

function syncNumber() {
if(files != null) {
GUI.fileNumber.max = files.length-1;
GUI.fileName.textContent = files[GUI.fileNumber.value].name;
} else {
GUI.fileNumber.max = 0;
}
}

function updateNumber() {
if(GUI.fileNumber.checkValidity() && files != undefined) {
gotoFile(GUI.fileNumber.value);
}
}

function moveFile(delta) {
var n = 1*GUI.fileNumber.value + delta;
if(n >= GUI.fileNumber.min && n <= GUI.fileNumber.max) {
GUI.fileNumber.value = n;
gotoFile(n);
}
}

function gotoFile(n) {
GUI.fileName.textContent = files[n].name;
View.open(files[n]);
}

+ 65
- 0
js/View.js View File

@@ -0,0 +1,65 @@
import * as ALTO from XML.ALTO;
import File;
import GUI;
import Scoria;
import * as Async from UnitJS.Async;
import XML;

var currentALTOFile = null;
GUI.wcThreshold.addEventListener('change', refresh);
GUI.loadScoria.addEventListener('click', importScoria);
GUI.saveScoria.addEventListener('click', exportScoria);

return {
open: open
};

function open(file) {
Async.run(
Async.bind(
File.load(file),
Async.map(XML.parse),
Async.map(function(ALTOFile) {
currentALTOFile = ALTOFile;
refresh();
})
)
);
}

function refresh() {
if(currentALTOFile != undefined) {
ALTO.display(currentALTOFile);
}
}

function importScoria() {
Async.run(
Async.bind(
File.pick({accept:"text/csv,.csv", multiple: true}),
function(input) {
var loaders = [];
for(var i = 0; i < input.files.length; i++) {
loaders.push(File.load(input.files[i]));
}
return Async.parallel.apply(null, loaders);
},
Async.map(setFromFiles),
Async.map(refresh)
)
);
}

function setFromFiles(files) {
files.forEach(function(file) {
file.split('\n').slice(1).forEach(function(line) {
Scoria.setScoria(line);
});
});
}

function exportScoria() {
var column = ['ID'].concat(Scoria.getScoriae());
var data = 'data:text/csv,' + encodeURIComponent(column.join('\n'));
File.save(data, 'scoria.csv');
}

src/XML.js → js/XML.js View File

@@ -1,7 +1,8 @@
return {
appendTo: appendTo,
onNodes: onNodes,
parse: parse
parse: parse,
serialize: serialize
}

function appendTo(element) {
@@ -18,3 +19,8 @@ function parse(input) {
var domReader = new DOMParser();
return domReader.parseFromString(input, 'text/xml');
}

function serialize(xmlDoc) {
var serializer = new XMLSerializer();
return serializer.serializeToString(xmlDoc);
}

src/XML/ALTO.js → js/XML/ALTO.js View File

@@ -6,13 +6,10 @@ import * as Dom from UnitJS.Dom;
import {appendTo, onNodes} from XML;

return {
display: display,
edit: edit,
display: display
};

function ifPush(test, elem) {
return function(t) {return test ? t.concat(elem) : t;};
}

function display(ALTODoc) {
var printSpace = Element.make(ALTODoc.querySelector('Page PrintSpace'));
Element.setPrintSpace(printSpace);
@@ -50,9 +47,9 @@ function display(ALTODoc) {
var wc = string.getAttribute('WC');
var id = string.getAttribute('ID');
var dom = Dom.make('span', {
class: ifPush(Quality.isLow(wc), "lowQuality")(
ifPush(isScoria(id), "deleted")(["String"])
),
class: ['String']
.concat(Quality.isLow(wc) ? 'lowQuality' : [])
.concat(isScoria(id) ? 'deleted' : []),
onClick: function(e) {toggleWord(e.target)},
id: id,
textContent: string.getAttribute('CONTENT')
@@ -61,3 +58,27 @@ function display(ALTODoc) {
};
}
}

function edit(ALTODoc) {
var printSpace = ALTODoc.querySelector('Page PrintSpace');
editElement(printSpace);
return ALTODoc;
}

function editElement(xmlElement) {
if(xmlElement.tagName == 'String') {
return !isScoria(xmlElement.getAttribute('ID'));
} else {
editChildren(xmlElement, Element.schema[xmlElement.tagName]);
return xmlElement.children.length > 0;
}
}

function editChildren(xmlElement, childTag) {
for(var i = 0; i < xmlElement.children.length; i++) {
var child = xmlElement.children[i];
if(child.tagName == childTag && !editElement(child)) {
xmlElement.removeChild(child);
}
}
}

src/XML/ALTO/Element.js → js/XML/ALTO/Element.js View File

@@ -4,9 +4,16 @@ var printSpace = {
pxHeight: null
};

var schema = {
'PrintSpace': 'TextBlock',
'TextBlock': 'TextLine',
'TextLine': 'String'
};

return {
make: make,
printSpace: printSpace,
schema: schema,
setPrintSpace: setPrintSpace
};


+ 0
- 13
src/File.js View File

@@ -1,13 +0,0 @@
return {
load: load
};

function load(file) {
return function(f) {
var fileReader = new FileReader();
fileReader.addEventListener('load', function() {
f(fileReader.result);
})
fileReader.readAsText(file);
};
}

+ 0
- 36
src/Scoria.js View File

@@ -1,36 +0,0 @@
import saveScoria from GUI;

saveScoria.addEventListener('click', save);
var scoriae = {};

return {
isScoria: isScoria,
save: save,
toggleWord: toggleWord
};

function isScoria(wordId) {
return scoriae[wordId] || false;
}

function toggleWord(domElem, forceState) {
if(editMode.checked) {
var state = forceState != undefined ? forceState : !scoriae[domElem.id];
if(state) {
scoriae[domElem.id] = true;
domElem.classList.add('deleted');
} else {
delete scoriae[domElem.id];
domElem.classList.remove('deleted');
}
}
}

function save() {
var column = ['ID'];
for(var id in scoriae) {
column.push(id);
}
var data = encodeURIComponent(column.join('\n'));
saveScoria.href = 'data:text/csv,' + data;
}

+ 0
- 48
src/Toolbar/Page.js View File

@@ -1,48 +0,0 @@
import * as GUI from GUI;
import View;

GUI.inputFiles.addEventListener('change', function() {
GUI.fileNumber.value = 0;
syncNumber();
updateNumber();
View.loadCurrentFile();
});
GUI.fileNumber.addEventListener('change', updateNumber);
window.addEventListener('keydown', function(e) {
switch(e.keyCode) {
case 0x25: moveFile(-1); break;
case 0x27: moveFile(+1); break;
}
});

return {
syncNumber: syncNumber
}

function syncNumber() {
if(GUI.inputFiles.files.length > 0) {
GUI.fileNumber.max = GUI.inputFiles.files.length-1;
GUI.fileName.textContent = GUI.inputFiles.files[GUI.fileNumber.value].name;
} else {
GUI.fileNumber.max = 0;
}
}

function updateNumber() {
if(GUI.fileNumber.checkValidity()) {
gotoFile(GUI.fileNumber.value);
}
}

function moveFile(delta) {
var n = 1*GUI.fileNumber.value + delta;
if(n >= GUI.fileNumber.min && n <= GUI.fileNumber.max) {
GUI.fileNumber.value = n;
gotoFile(n);
}
}

function gotoFile(n) {
GUI.fileName.textContent = GUI.inputFiles.files[n].name;
View.loadCurrentFile();
}

+ 0
- 31
src/View.js View File

@@ -1,31 +0,0 @@
import * as ALTO from XML.ALTO;
import File;
import GUI;
import * as Async from UnitJS.Async;
import XML;

var currentALTOFile = null;
wcThreshold.addEventListener('change', refresh);

return {
loadCurrentFile: loadCurrentFile
};

function loadCurrentFile() {
Async.run(
Async.bind(
File.load(inputFiles.files[GUI.fileNumber.value]),
Async.map(XML.parse),
Async.map(function(ALTOFile) {
currentALTOFile = ALTOFile;
refresh();
})
)
);
}

function refresh() {
if(currentALTOFile != undefined) {
ALTO.display(currentALTOFile);
}
}

Loading…
Cancel
Save