Commit 6f628184 authored by Eva Bardou's avatar Eva Bardou Committed by Bastien Abadie
Browse files

Setup unique entrypoint from Django to vue.js

parent d7737938
......@@ -14,10 +14,6 @@
<a href="{% if document and models_count %}{% url 'document-models' document_pk=document.pk %}{% else %}#{% endif %}" class="nav-item nav-link {% if not document or not models_count %}disabled{% endif %}" id="nav-models-tab" role="tab" aria-controls="nav-doc" aria-selected="true">{% trans "Models" %}</a>
{% endwith %}
{% endif %}
<div class="nav-item ml-2">
{% if object %}<span id="part-name">{{ document.name }}</span>{% endif %}
{% block extra_info %}{% endblock %}
</div>
{% block extra_nav %}{% endblock %}
</div>
</nav>
......
{% extends 'core/document_nav.html' %}
{% load i18n staticfiles %}
{% block nav-edit-active %}active{% endblock %}
{% block extra_info %}
<span id="part-title" v-if="part.loaded">${part.title} - ${part.filename} - (${imageSize})</span>
<span class="loading" v-if="!part.loaded">{% trans "Loading&#8230;" %}</span>
{% extends 'base.html' %}
{% load i18n staticfiles bootstrap %}
{% block head_title %}{% if object %}{% trans "Update a Document" %}{% else %}{% trans "Create a new Document" %}{% endif %}{% endblock %}
{% block body %}
<div id="editor">
<editor :object="'{{object}}'"
:document-id="'{{document.id}}'"
:document-name="'{{document.name}}'"
:part-id="'{{part.id}}'"
:default-text-direction="'{{ document.default_text_direction }}'"
:main-text-direction="'{{ document.main_script.text_direction }}'"
:read-direction="'{{ document.read_direction }}'">
<a href="{% if object %}{% url 'document-update' pk=document.pk %}{% endif %}" class="nav-item nav-link {% block nav-doc-active %}{% endblock %}" id="nav-doc-tab" role="tab" aria-controls="nav-doc" aria-selected="true">{% trans "Description" %}</a>
<a href="{% if object %}{% url 'document-images' pk=document.pk %}{% else %}#{% endif %}" class="nav-item nav-link {% if not object %}disabled{% endif %} {% block nav-images-active %}{% endblock %}" id="nav-img-tab" role="tab" aria-controls="nav-img" aria-selected="true">{% trans "Images" %}</a>
<a href="{% if document %}{% url 'document-part-edit' pk=document.pk %}{% else %}#{% endif %}" class="nav-item nav-link active {% if not object or not document.parts.count %}disabled{% endif %}" id="nav-edit-tab" role="tab" aria-controls="nav-doc" aria-selected="true">{% trans "Edit" %}</a>
{% if perms.core.can_train %}
{% with models_count=document.ocr_models.count %}
<a href="{% if document and models_count %}{% url 'document-models' document_pk=document.pk %}{% else %}#{% endif %}" class="nav-item nav-link {% if not document or not models_count %}disabled{% endif %}" id="nav-models-tab" role="tab" aria-controls="nav-doc" aria-selected="true">{% trans "Models" %}</a>
{% endwith %}
{% endif %}
</editor>
</div>
<div v-if="part.loaded" class="nav-item form-inline ml-auto mr-auto">
<select v-model="selectedTranscription"
id="document-transcriptions"
title="{% trans 'Transcription' %}"
class="form-control custom-select">
<option v-for="transcription in part.transcriptions"
v-bind:key="transcription.pk"
v-bind:value="transcription.pk">${transcription.name}</option>
</select>
<button type="button"
class="btn btn-primary fas fa-cog ml-1"
title="{% trans "Transcription management" %}"
data-toggle="modal"
data-target="#transcriptionsManagement"></button>
<div id="transcriptionsManagement"
class="modal ui-draggable"
tabindex="-1"
role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header ui-draggable-handle">
<h5 class="modal-title">{% trans "Transcriptions management" %}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div>
<span>{% trans "Compare" %}</span>
<span class="float-right">{% trans "Delete" %}</span>
</div>
<div v-for="trans in part.transcriptions"
v-bind="trans"
v-bind:key="trans.pk"
class="inline-form form-check mt-1">
<input type="checkbox" class="form-check-input"
v-bind:id="'opt' + trans.pk"
v-model="comparedTranscriptions"
v-bind:value="trans.pk" />
<label v-bind:for="'opt'+trans.pk"
class="form-check-label col">${trans.name}</label>
<button v-bind:disabled="trans.name == 'manual' || trans.pk == selectedTranscription"
v-bind:data-trPk="trans.pk"
@click="deleteTranscription"
class="btn btn-danger fas fa-trash"
title="{% trans "Completely remove the transcription and all of it's content!&#10;You can not remove the manual or the current transcription." %}"></button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_nav %}
<div class="nav-item ml-auto" id="toggle-panels">
<button type="button"
id="source-panel-btn"
@click="togglePanel"
data-target="source"
class="open-panel nav-item btn"
v-bind:class="[ show.source ? 'btn-primary' : 'btn-secondary' ]"
title="{% trans "Source Image" %}"><i class="click-through fas fa-eye"></i></button>
<button type="button"
id="seg-panel-btn"
@click="togglePanel"
data-target="segmentation"
class="open-panel nav-item btn"
v-bind:class="[ show.segmentation ? 'btn-primary' : 'btn-secondary' ]"
title="{% trans "Segmentation" %}"><i class="click-through fas fa-align-left"></i></button>
<button type="button"
id="trans-panel-btn"
@click="togglePanel"
data-target="visualisation"
class="open-panel nav-item btn"
v-bind:class="[ show.visualisation ? 'btn-primary' : 'btn-secondary' ]"
title="{% trans "Transcription" %}"><i class="click-through fas fa-language"></i></button>
<button type="button"
id="diplo-panel-btn"
@click="togglePanel"
data-target="diplomatic"
class="open-panel nav-item btn"
v-bind:class="[ show.diplomatic ? 'btn-primary' : 'btn-secondary' ]"
title="{% trans "Text" %}"><i class="click-through fas fa-list-ol"></i></button>
</div>
{% endblock %}
{% block tab_content %}
<div class="row">
<div class="col-sides">
{% if document.read_direction == 'rtl' %}
<a id="next-part"
v-if="part.next"
href="#"
@click="getNext"
class="nav-btn nav-next"
title="{% trans "Next (Page Down or Ctrl+Left Arrow)" %}">
<i class="fas fa-angle-left"></i></a>
{% else %}
<a id="prev-part"
v-if="part.previous"
href="#"
@click="getPrevious"
class="nav-btn nav-prev"
title="{% trans "Previous (Page Up or Ctrl+Right Arrow)" %}">
<i class="fas fa-angle-left"></i></a>
{% endif %}
<div>
<button id="zoom-reset"
@click="resetZoom"
class="btn btn-sm btn-info"
title="{% trans "Reset zoom. (Ctrl+Backspace)" %}">
<i class="fas fa-search-minus"></i>
</button>
<input id="zoom-range"
type="range"
name="zoom-range"
class="form-control-range mt-1"
orient="vertical"
v-bind:min="zoom.minScale"
v-bind:max="zoom.maxScale"
v-model="zoomScale"
step="0.3">
</div>
</div>
<SourcePanel v-if="show.source && part.loaded"
v-bind:part="part"
v-bind:fullsizeimage="fullsizeimage"
ref="sourcePanel">
</SourcePanel>
<keep-alive>
<SegmentationPanel v-if="show.segmentation && part.loaded"
v-bind:fullsizeimage="fullsizeimage"
v-bind:part="part"
id="segmentation-panel"
ref="segPanel">
</SegmentationPanel>
</keep-alive>
<keep-alive>
<VisuPanel v-if="show.visualisation && part.loaded"
v-bind:part="part"
v-bind:default-text-direction="'{{ document.default_text_direction }}'"
v-bind:read-direction="'{{ document.read_direction }}'"
id="transcription-panel"
ref="visuPanel">
</VisuPanel>
</keep-alive>
<keep-alive>
<DiploPanel id="diplo-panel"
v-bind:part="part"
v-if="show.diplomatic && part.loaded"
ref="diploPanel">
</DiploPanel>
</keep-alive>
<div class="col-sides">
{% if document.read_direction == 'rtl' %}
<a id="prev-part"
v-if="part.previous"
@click="getPrevious"
href="#"
class="nav-btn nav-prev"
title="{% trans "Previous (Page Up or Ctrl+Left Arrow)" %}">
<i class="fas fa-angle-right"></i>
</a>
{% else %}
<a id="next-part"
v-if="part.next" @click="getNext"
href="#"
class="nav-btn nav-next"
title="{% trans "Next (Page Down or Ctrl+Right Arrow)" %}">
<i class="fas fa-angle-right"></i>
</a>
{% endif %}
</div>
</div>
{% endblock %}
{% block scripts %}
{% endblock %}
<script type="text/javascript">
const READ_DIRECTION = '{{document.read_direction}}';
const TEXT_DIRECTION = '{{document.main_script.text_direction}}';
const DOCUMENT_ID = '{{document.id}}';
var PART_ID = {{part.id}}; // can be changed with next & previous pages
{% if user.onboarding %}
const ONBOARDING_PAGE = "onboarding_edit";
{% endif %}
var models_url = "{% url 'document-models' document_pk=document.pk %}";
</script>
{% block scripts %}
<script type="text/javascript">
{% if user.onboarding %}
const ONBOARDING_PAGE = "onboarding_edit";
{% endif %}
</script>
{{ block.super }}
{{ block.super }}
<script type="text/javascript">
$(document).ready(function() {
......
......@@ -367,6 +367,9 @@ form.inline-form {
/* transcription interface */
.nav-div {
display: inline-block;
}
.nav-btn {
font-size: 50px;
......
window.Vue = require('vue/dist/vue');
import { partStore } from './store/part.js';
import SourcePanel from '../../vue/components/SourcePanel.vue';
import SegPanel from '../../vue/components/SegPanel.vue';
import VisuPanel from '../../vue/components/VisuPanel.vue';
import DiploPanel from '../../vue/components/DiploPanel.vue';
import Editor from '../../vue/components/Editor.vue';
export var partVM = new Vue({
el: "#part-edit",
delimiters: ["${","}"],
data: {
part: partStore,
zoom: new WheelZoom(),
show: {
source: userProfile.get('source-panel'),
segmentation: userProfile.get('segmentation-panel'),
visualisation: userProfile.get('visualisation-panel'),
diplomatic: userProfile.get('diplomatic-panel')
},
blockShortcuts: false,
fullsizeimage: false,
selectedTranscription: null,
comparedTranscriptions: []
},
computed: {
imageSize() {
return this.part.image.size[0]+'x'+this.part.image.size[1];
},
openedPanels() {
return [this.show.source,
this.show.segmentation,
this.show.visualisation].filter(p=>p===true);
},
zoomScale: {
get() {
return this.zoom.scale || 1;
},
set(newValue) {
let target = {
x: this.$el.clientWidth/this.openedPanels.length/2-this.zoom.pos.x,
y: this.$el.clientHeight/this.openedPanels.length/2-this.zoom.pos.y
};
this.zoom.zoomTo(target, parseFloat(newValue)-this.zoom.scale);
}
}
},
watch: {
'part.pk': function(n, o) {
if (n) {
// set the new url
window.history.pushState(
{}, "",
document.location.href.replace(/(part\/)\d+(\/edit)/,
'$1'+this.part.pk+'$2'));
// set the 'image' tab btn to select the corresponding image
var tabUrl = new URL($('#nav-img-tab').attr('href'),
window.location.origin);
tabUrl.searchParams.set('select', this.part.pk);
$('#nav-img-tab').attr('href', tabUrl);
}
},
selectedTranscription: function(n, o) {
let itrans = userProfile.get('initialTranscriptions') || {};
itrans[DOCUMENT_ID] = n;
userProfile.set('initialTranscriptions', itrans);
this.getCurrentContent(n);
},
comparedTranscriptions: function(n, o) {
n.forEach(function(tr, i) {
if (!o.find(e=>e==tr)) {
this.part.fetchContent(tr);
}
}.bind(this));
},
blockShortcuts(n, o) {
// make sure the segmenter doesnt trigger keyboard shortcuts either
if (this.$refs.segPanel) this.$refs.segPanel.segmenter.disableShortcuts = n;
}
},
el: "#editor",
components: {
'sourcepanel': SourcePanel,
'segmentationpanel': SegPanel,
'visupanel': VisuPanel,
'diplopanel': DiploPanel,
},
created() {
// this.fetch();
this.part.fetchPart(PART_ID, function() {
let tr = userProfile.get('initialTranscriptions')
&& userProfile.get('initialTranscriptions')[DOCUMENT_ID]
|| this.part.transcriptions[0].pk;
this.selectedTranscription = tr;
}.bind(this));
// bind all events emited from panels and such
this.$on('update:transcription', function(lineTranscription) {
this.part.pushContent(lineTranscription);
}.bind(this));
this.$on('create:line', function(line, cb) {
this.part.createLine(line, this.selectedTranscription, cb);
}.bind(this));
this.$on('bulk_create:lines', function(line, cb) {
this.part.bulkCreateLines(line, this.selectedTranscription, cb);
}.bind(this));
this.$on('update:line', function(line, cb) {
this.part.updateLine(line, cb);
}.bind(this));
this.$on('bulk_update:lines', function(lines, cb) {
this.part.bulkUpdateLines(lines, cb);
}.bind(this));
this.$on('delete:line', function(linePk, cb) {
this.part.deleteLine(linePk, cb);
}.bind(this));
this.$on('bulk_delete:lines', function(pks, cb) {
this.part.bulkDeleteLines(pks, cb);
}.bind(this));
this.$on('create:region', function(region, cb) {
this.part.createRegion(region, cb);
}.bind(this));
this.$on('update:region', function(region, cb) {
this.part.updateRegion(region, cb);
}.bind(this));
this.$on('delete:region', function(regionPk, cb) {
this.part.deleteRegion(regionPk, cb);
}.bind(this));
this.$on('bulk_create:transcriptions', function(lines, cb) {
this.part.bulkCreateLineTranscriptions(lines, cb);
}.bind(this));
this.$on('bulk_update:transcriptions', function(lines, cb) {
this.part.bulkUpdateLineTranscriptions(lines, cb);
}.bind(this));
this.$on('move:line', function(movedLines, cb) {
this.part.move(movedLines, cb);
}.bind(this));
document.addEventListener('keydown', function(event) {
if (this.blockShortcuts) return;
if (event.keyCode == 33 || // page up
(event.keyCode == (READ_DIRECTION == 'rtl'?39:37) && event.ctrlKey)) { // arrow left
this.getPrevious();
event.preventDefault();
} else if (event.keyCode == 34 || // page down
(event.keyCode == (READ_DIRECTION == 'rtl'?37:39) && event.ctrlKey)) { // arrow right
this.getNext();
event.preventDefault();
} else if (event.keyCode == 8 && event.ctrlKey) { // backspace
this.zoom.reset();
}
}.bind(this));
let debounced = _.debounce(function() { // avoid calling this too often
if(this.$refs.segPanel) this.$refs.segPanel.refresh();
if(this.$refs.visuPanel) this.$refs.visuPanel.refresh();
if(this.$refs.diploPanel) this.$refs.diploPanel.refresh();
}.bind(this), 200);
window.addEventListener('resize', debounced);
// load the full size image when we reach a scale > 1
this.zoom.events.addEventListener('wheelzoom.updated', function(ev) {
if (this.part.loaded && !this.fullsizeimage) {
let ratio = ev.target.clientWidth / this.part.image.size[0];
if (this.zoom.scale * ratio > 1) {
this.prefetchImage(this.part.image.uri, function() {
this.fullsizeimage = true;
}.bind(this));
}
}
}.bind(this));
// catch background emited events when masks are recalculated
let $alertsContainer = $('#alerts-container');
$alertsContainer.on('part:mask', function(ev, data) {
data.lines.forEach(function(lineData) {
let line = this.part.lines.find(l=>l.pk == lineData.pk);
if (line) { // might have been deleted in the meantime
line.mask = lineData.mask;
}
}.bind(this));
}.bind(this));
},
methods: {
resetZoom() {
this.zoom.reset();
},
prefetchImage(src, callback) {
// this is the panel's responsability to call this!
let img = new Image();
img.addEventListener('load', function() {
if (callback) callback(src);
img.remove();
}.bind(this));
img.src = src;
},
deleteTranscription(ev) {
let transcription = ev.target.dataset.trpk;
// I lied, it's only archived
if(confirm("Are you sure you want to delete the transcription?")) {
this.part.archiveTranscription(transcription);
ev.target.parentNode.remove(); // meh
let compInd = this.comparedTranscriptions.findIndex(e=>e.pk == transcription);
if (compInd != -1) Vue.delete(this.comparedTranscriptions, compInd)
}
},
getCurrentContent(transcription) {
this.part.fetchContent(transcription, function() {
this.part.lines.forEach(function(line, i) {
if (line.transcriptions[this.selectedTranscription]) {
Vue.set(line, 'currentTrans',
line.transcriptions[this.selectedTranscription]);
}
}.bind(this));
}.bind(this));
},
getComparisonContent() {
this.comparedTranscriptions.forEach(function(tr, i) {
if (tr != this.selectedTranscription) {
this.part.fetchContent(tr);
}
}.bind(this));
},
getPrevious(ev) {
return this.part.getPrevious(function() {
this.getCurrentContent(this.selectedTranscription);
this.getComparisonContent();
}.bind(this));
},
getNext(ev) {
return this.part.getNext(function() {
this.getCurrentContent(this.selectedTranscription);
this.getComparisonContent();
}.bind(this));
},
togglePanel(ev) {
let btn = ev.target;
let target = btn.getAttribute('data-target');
this.show[target] = !this.show[target];
userProfile.set(target + '-panel', this.show[target]);
// wait for css
Vue.nextTick(function() {
if(this.$refs.segPanel) this.$refs.segPanel.refresh();
if(this.$refs.visuPanel) this.$refs.visuPanel.refresh();
if(this.$refs.diploPanel) this.$refs.diploPanel.refresh();
}.bind(this));
},
'editor': Editor,
}
});
// singleton!
export const partStore = {
// need to set empty value for vue to watch them
documentId: null,
loaded: false,
pk: null,
lines: [],
......@@ -38,7 +39,7 @@ export const partStore = {
// api
getApiRoot() {
return '/api/documents/' + DOCUMENT_ID + '/';
return '/api/documents/' + this.documentId + '/';
},
getApiPart(pk) {
return this.getApiRoot() + 'parts/' + (pk?pk:this.pk) + '/';
......
......@@ -146,10 +146,10 @@ export default BasePanel.extend({
}
},
startEdit(ev) {
this.$parent.blockShortcuts = true;
this.$parent.$parent.blockShortcuts = true;
},
stopEdit(ev) {
this.$parent.blockShortcuts = false;
this.$parent.$parent.blockShortcuts = false;
this.constrainLineNumber();
this.save();
},
......@@ -177,7 +177,7 @@ export default BasePanel.extend({
},
moveLines() {
if(this.movedLines.length != 0){
this.$parent.$emit('move:line', this.movedLines, function () {
this.$parent.$parent.$emit('move:line', this.movedLines, function () {
this.movedLines = [];
}.bind(this));
}
......@@ -288,7 +288,7 @@ export default BasePanel.extend({
bulkUpdate() {
if(this.updatedLines.length){
this.$parent.$emit(
this.$parent.$parent.$emit(
'bulk_update:transcriptions',
this.updatedLines,
function () {
......@@ -298,7 +298,7 @@ export default BasePanel.extend({
},
bulkCreate() {
if(this.createdLines.length){
this.$parent.$emit(
this.$parent.$parent.$emit(
'bulk_create:transcriptions',
this.createdLines,
function () {
......
<template>
<div>
<nav>
<div class="nav nav-tabs mb-3" id="nav-tab" role="tablist">
<slot></slot>
<extrainfo :object="object"
:document-name="documentName"
:part="part"
@delete-transcription="deleteTranscription">
</extrainfo>
<extranav :show="show"></extranav>