Change directory structure (move ./nopaque/* to ./)

This commit is contained in:
Patrick Jentsch committed 2021-07-20 15:07:42 +02:00
1 parent ff39d8d650
commit d6ab379418
231 files changed
+26 -23

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
/* https://google.github.io/material-design-icons/#setup-method-2-self-hosting */
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url(../fonts/material_icons/MaterialIcons-Regular.ttf) format('truetype'),
url(../fonts/material_icons/MaterialIconsOutlined-Regular.otf) format('opentype'),
url(../fonts/material_icons/MaterialIconsRound-Regular.otf) format('opentype'),
url(../fonts/material_icons/MaterialIconsSharp-Regular.otf) format('opentype'),
url(../fonts/material_icons/MaterialIconsTwoTone-Regular.otf) format('opentype');
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px; /* Preferred icon size */
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
/* Support for IE. */
font-feature-settings: 'liga';
}
+13
View File
@@ -0,0 +1,13 @@
/*!
* Materialize v1.0.0 (http://materializecss.com)
* Copyright 2014-2017 Materialize
* MIT License (https://raw.githubusercontent.com/Dogfalo/materialize/master/LICENSE)
*/
.materialize-red{background-color:#e51c23 !important}.materialize-red-text{color:#e51c23 !important}.materialize-red.lighten-5{background-color:#fdeaeb !important}.materialize-red-text.text-lighten-5{color:#fdeaeb !important}.materialize-red.lighten-4{background-color:#f8c1c3 !important}.materialize-red-text.text-lighten-4{color:#f8c1c3 !important}.materialize-red.lighten-3{background-color:#f3989b !important}.materialize-red-text.text-lighten-3{color:#f3989b !important}.materialize-red.lighten-2{background-color:#ee6e73 !important}.materialize-red-text.text-lighten-2{color:#ee6e73 !important}.materialize-red.lighten-1{background-color:#ea454b !important}.materialize-red-text.text-lighten-1{color:#ea454b !important}.materialize-red.darken-1{background-color:#d0181e !important}.materialize-red-text.text-darken-1{color:#d0181e !important}.materialize-red.darken-2{background-color:#b9151b !important}.materialize-red-text.text-darken-2{color:#b9151b !important}.materialize-red.darken-3{background-color:#a21318 !important}.materialize-red-text.text-darken-3{color:#a21318 !important}.materialize-red.darken-4{background-color:#8b1014 !important}.materialize-red-text.text-darken-4{color:#8b1014 !important}.red{background-color:#F44336 !important}.red-text{color:#F44336 !important}.red.lighten-5{background-color:#FFEBEE !important}.red-text.text-lighten-5{color:#FFEBEE !important}.red.lighten-4{background-color:#FFCDD2 !important}.red-text.text-lighten-4{color:#FFCDD2 !important}.red.lighten-3{background-color:#EF9A9A !important}.red-text.text-lighten-3{color:#EF9A9A !important}.red.lighten-2{background-color:#E57373 !important}.red-text.text-lighten-2{color:#E57373 !important}.red.lighten-1{background-color:#EF5350 !important}.red-text.text-lighten-1{color:#EF5350 !important}.red.darken-1{background-color:#E53935 !important}.red-text.text-darken-1{color:#E53935 !important}.red.darken-2{background-color:#D32F2F !important}.red-text.text-darken-2{color:#D32F2F !important}.red.darken-3{background-color:#C62828 !important}.red-text.text-darken-3{color:#C62828 !important}.red.darken-4{background-color:#B71C1C !important}.red-text.text-darken-4{color:#B71C1C !important}.red.accent-1{background-color:#FF8A80 !important}.red-text.text-accent-1{color:#FF8A80 !important}.red.accent-2{background-color:#FF5252 !important}.red-text.text-accent-2{color:#FF5252 !important}.red.accent-3{background-color:#FF1744 !important}.red-text.text-accent-3{color:#FF1744 !important}.red.accent-4{background-color:#D50000 !important}.red-text.text-accent-4{color:#D50000 !important}.pink{background-color:#e91e63 !important}.pink-text{color:#e91e63 !important}.pink.lighten-5{background-color:#fce4ec !important}.pink-text.text-lighten-5{color:#fce4ec !important}.pink.lighten-4{background-color:#f8bbd0 !important}.pink-text.text-lighten-4{color:#f8bbd0 !important}.pink.lighten-3{background-color:#f48fb1 !important}.pink-text.text-lighten-3{color:#f48fb1 !important}.pink.lighten-2{background-color:#f06292 !important}.pink-text.text-lighten-2{color:#f06292 !important}.pink.lighten-1{background-color:#ec407a !important}.pink-text.text-lighten-1{color:#ec407a !important}.pink.darken-1{background-color:#d81b60 !important}.pink-text.text-darken-1{color:#d81b60 !important}.pink.darken-2{background-color:#c2185b !important}.pink-text.text-darken-2{color:#c2185b !important}.pink.darken-3{background-color:#ad1457 !important}.pink-text.text-darken-3{color:#ad1457 !important}.pink.darken-4{background-color:#880e4f !important}.pink-text.text-darken-4{color:#880e4f !important}.pink.accent-1{background-color:#ff80ab !important}.pink-text.text-accent-1{color:#ff80ab !important}.pink.accent-2{background-color:#ff4081 !important}.pink-text.text-accent-2{color:#ff4081 !important}.pink.accent-3{background-color:#f50057 !important}.pink-text.text-accent-3{color:#f50057 !important}.pink.accent-4{background-color:#c51162 !important}.pink-text.text-accent-4{color:#c51162 !important}.purple{background-color:#9c27b0 !important}.purple-text{color:#9c27b0 !important}.purple.lighten-5{background-color:#f3e5f5 !important}.purple-text.text-lighten-5{color:#f3e5f5 !important}.purple.lighten-4{background-color:#e1bee7 !important}.purple-text.text-lighten-4{color:#e1bee7 !important}.purple.lighten-3{background-color:#ce93d8 !important}.purple-text.text-lighten-3{color:#ce93d8 !important}.purple.lighten-2{background-color:#ba68c8 !important}.purple-text.text-lighten-2{color:#ba68c8 !important}.purple.lighten-1{background-color:#ab47bc !important}.purple-text.text-lighten-1{color:#ab47bc !important}.purple.darken-1{background-color:#8e24aa !important}.purple-text.text-darken-1{color:#8e24aa !important}.purple.darken-2{background-color:#7b1fa2 !important}.purple-text.text-darken-2{color:#7b1fa2 !important}.purple.darken-3{background-color:#6a1b9a !important}.purple-text.text-darken-3{color:#6a1b9a !important}.purple.darken-4{background-color:#4a148c !important}.purple-text.text-darkeLine truncated
* Waves v0.6.0
* http://fian.my.id/Waves
*
* Copyright 2014 Alfiana E. Sibuea and other contributors
* Released under the MIT license
* https://github.com/fians/Waves/blob/master/LICENSE
*/.waves-effect{position:relative;cursor:pointer;display:inline-block;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;vertical-align:middle;z-index:1;-webkit-transition:.3s ease-out;transition:.3s ease-out}.waves-effect .waves-ripple{position:absolute;border-radius:50%;width:20px;height:20px;margin-top:-10px;margin-left:-10px;opacity:0;background:rgba(0,0,0,0.2);-webkit-transition:all 0.7s ease-out;transition:all 0.7s ease-out;-webkit-transition-property:opacity, -webkit-transform;transition-property:opacity, -webkit-transform;transition-property:transform, opacity;transition-property:transform, opacity, -webkit-transform;-webkit-transform:scale(0);transform:scale(0);pointer-events:none}.waves-effect.waves-light .waves-ripple{background-color:rgba(255,255,255,0.45)}.waves-effect.waves-red .waves-ripple{background-color:rgba(244,67,54,0.7)}.waves-effect.waves-yellow .waves-ripple{background-color:rgba(255,235,59,0.7)}.waves-effect.waves-orange .waves-ripple{background-color:rgba(255,152,0,0.7)}.waves-effect.waves-purple .waves-ripple{background-color:rgba(156,39,176,0.7)}.waves-effect.waves-green .waves-ripple{background-color:rgba(76,175,80,0.7)}.waves-effect.waves-teal .waves-ripple{background-color:rgba(0,150,136,0.7)}.waves-effect input[type="button"],.waves-effect input[type="reset"],.waves-effect input[type="submit"]{border:0;font-style:normal;font-size:inherit;text-transform:inherit;background:none}.waves-effect img{position:relative;z-index:-1}.waves-notransition{-webkit-transition:none !important;transition:none !important}.waves-circle{-webkit-transform:translateZ(0);transform:translateZ(0);-webkit-mask-image:-webkit-radial-gradient(circle, white 100%, black 100%)}.waves-input-wrapper{border-radius:0.2em;vertical-align:bottom}.waves-input-wrapper .waves-button-input{position:relative;top:0;left:0;z-index:1}.waves-circle{text-align:center;width:2.5em;height:2.5em;line-height:2.5em;border-radius:50%;-webkit-mask-image:none}.waves-block{display:block}.waves-effect .waves-ripple{z-index:-1}.modal{display:none;position:fixed;left:0;right:0;background-color:#fafafa;padding:0;max-height:70%;width:55%;margin:auto;overflow-y:auto;border-radius:2px;will-change:top, opacity}.modal:focus{outline:none}@media only screen and (max-width: 992px){.modal{width:80%}}.modal h1,.modal h2,.modal h3,.modal h4{margin-top:0}.modal .modal-content{padding:24px}.modal .modal-close{cursor:pointer}.modal .modal-footer{border-radius:0 0 2px 2px;background-color:#fafafa;padding:4px 6px;height:56px;width:100%;text-align:right}.modal .modal-footer .btn,.modal .modal-footer .btn-large,.modal .modal-footer .btn-small,.modal .modal-footer .btn-flat{margin:6px 0}.modal-overlay{position:fixed;z-index:999;top:-25%;left:0;bottom:0;right:0;height:125%;width:100%;background:#000;display:none;will-change:opacity}.modal.modal-fixed-footer{padding:0;height:70%}.modal.modal-fixed-footer .modal-content{position:absolute;height:calc(100% - 56px);max-height:100%;width:100%;overflow-y:auto}.modal.modal-fixed-footer .modal-footer{border-top:1px solid rgba(0,0,0,0.1);position:absolute;bottom:0}.modal.bottom-sheet{top:auto;bottom:-100%;margin:0;width:100%;max-height:45%;border-radius:0;will-change:bottom, opacity}.collapsible{border-top:1px solid #ddd;border-right:1px solid #ddd;border-left:1px solid #ddd;margin:.5rem 0 1rem 0}.collapsible-header{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;cursor:pointer;-webkit-tap-highlight-color:transparent;line-height:1.5;padding:1rem;background-color:#fff;border-bottom:1px solid #ddd}.collapsible-header:focus{outline:0}.collapsible-header i{width:2rem;font-size:1.6rem;display:inline-block;text-align:center;margin-right:1rem}.keyboard-focused .collapsible-header:focus{background-color:#eee}.collapsible-body{display:none;border-bottom:1px solid #ddd;-webkit-box-sizing:border-box;box-sizing:border-box;padding:2rem}.sidenav .collapsible,.sidenav.fixed .collapsible{border:none;-webkit-box-shadow:none;box-shadow:none}.sidenav .collapsible li,.sidenav.fixed .collapsible li{padding:0}.sidenav .collapsible-header,.sidenav.fixed .collapsible-header{background-color:transparent;border:none;line-height:inherit;height:inherit;padding:0 16px}.sidenav .collapsible-header:hover,.sidenav.fixed .collapsible-header:hover{background-color:rgba(0,0,0,0.05)}.sidenav .collapsible-header i,.sidenav.fixed .collapsible-header i{line-height:inherit}.sidenav .collapsible-body,.sidenav.fixed .collapsible-body{border:0;background-color:#fff}.sidenav .collapsible-body li a,.sidenav.fixed .collapsible-body li a{padding:0 23.5px 0 31px}.collapsible.popout{border:none;-webkit-box-shadow:none;box-shadow:none}.collapsible.popout>li{-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);margin:0 24px;-webkit-transition:margin 0.35s cubLine truncated
+9
View File
@@ -0,0 +1,9 @@
/* Fix material icon vertical alignment when nested in various elements */
h1 .nopaque-icons, h2 .nopaque-icons, h3 .nopaque-icons, h4 .nopaque-icons,
.tab .nopaque-icons, .tab .material-icons {
line-height: inherit;
}
.parallax-container .parallax {
z-index: 0;
}
+109
View File
@@ -0,0 +1,109 @@
/* Change navbar height bacause an extended and fixed navbar is used */
.navbar-fixed {
height: 112px;
}
/* add custom bold class */
.bold {font-weight: bold;}
/* Change placholdertext color of file uplaod fields */
::placeholder {
color: #9e9e9e;
opacity: 1;
}
/* preloader circle in the size of a button icon */
.button-icon-spinner {
bottom: -5px !important;
right: 55px !important;
margin-right: 12px !important;
width: 19.5px !important;
height: 19.5px !important;
}
/*
* changes preoloader size etc. to fit visually better with the chip status
* indicator of jobs
*/
.status-spinner {
margin-bottom: -10px;
width: 30px !important;
height: 30px !important;
}
/* flat-interaction addition to show background color */
.flat-interaction {
background-color: #DCDCDC;
width: 100%;
margin-bottom: 3px;
text-transform: capitalize;
}
.flat-interaction:hover {
background-color: #E6E6FA !important;
}
/* CSS for clickable th elements in tables. Needed for sortable table data with
list js. On click on th header elements will be sorted accordingly. Also a caret
indicator will show up how the column is sorted right now.; */
.sort {
cursor: pointer;
}
.sort:after {
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 5px solid transparent;
content:"";
position: relative;
top:-10px;
right:-5px;
}
.sort.asc:after {
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 5px solid #000000;
content:"";
position: relative;
top:13px;
right:-5px;
}
.sort.desc:after {
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 5px solid #000000;
content:"";
position: relative;
top:-10px;
right:-5px;
}
.show-if-only-child:not(:only-child) {
display: none !important;
}
/* class for expert view */
.expert-view {
cursor: pointer;
}
.btn-scale-x2 {
transform: scale(2);
}
.btn-scale-x2 .nopaque-icons.service-icon {
font-size: 2.5rem;
}
.nopaque-icons.service-icon[data-service="corpus-analysis"]:empty:before {content: "H";}
.nopaque-icons.service-icon[data-service="file-setup"]:empty:before {content: "E";}
.nopaque-icons.service-icon[data-service="nlp"]:empty:before {content: "G";}
.nopaque-icons.service-icon[data-service="ocr"]:empty:before {content: "F";}
.status-text[data-status]:empty:before {content: attr(data-status);}
+33
View File
@@ -0,0 +1,33 @@
@font-face {
font-family: 'nopaque Icons';
font-style: normal;
font-weight: 400;
src: local('nopaque Icons'),
local('nopaqueIcons-Regular'),
url(../fonts/nopaque_icons/nopaqueIcons-Regular.otf) format('opentype');
}
.nopaque-icons {
font-family: 'nopaque Icons';
font-weight: normal;
font-style: normal;
font-size: 24px; /* Preferred icon size */
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
/* Support for IE. */
font-feature-settings: 'liga';
}
+12
View File
@@ -0,0 +1,12 @@
/*
* The sidenav-fixed class is used which causes the sidenav to be fixed and open
* on large screens and hides to the regular functionality on smaller screens.
* In order to prevent the sidenav to overlap the content, the content (in our
* case header, main and footer) gets an offset equal to the width of the
* sidenav.
*/
@media only screen and (min-width : 993px) {
header, main, footer {padding-left: 300px;}
.modal:not(.bottom-sheet) {left: 300px;}
.navbar-fixed > nav {width: calc(100% - 300px)}
}
+18
View File
@@ -0,0 +1,18 @@
/*
* Sticky Footer: https://materializecss.com/footer.html#sticky-footer
* A sticky footer always stays on the bottom of the page regardless of how
* little content is on the page. However, this footer will be pushed down if
* there is a lot of content, so it is different from a fixed footer.
*
* Note: This may cause issues in Internet Explorer which has weak support for
* flexbox.
*/
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
main {
flex: 1 0 auto;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

+1
View File
@@ -0,0 +1 @@
<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" width="66mm" height="64mm" viewBox="0 0 187.09 181.42"><rect x="56.69" y="56.69" width="70.87" height="59.68" style="fill:none"/><g id="Bildmarke_RZ" data-name="Bildmarke RZ"><path d="M119.59,87.15S108.84,98.28,92.13,98.28,64.66,87.15,64.66,87.15,75.44,76,92.13,76,119.59,87.15,119.59,87.15Z" style="fill:#fff;opacity:0.46"/><polygon points="83.77 106.92 102.88 87.15 83.77 67.38 64.66 87.15 83.77 106.92" style="fill:#fff;opacity:0.35000000000000003"/><polygon points="100.48 106.92 119.59 87.15 100.48 67.38 81.37 87.15 100.48 106.92" style="fill:#fff;opacity:0.35000000000000003"/><polygon points="81.37 87.15 92.13 98.28 102.88 87.15 92.13 76.02 81.37 87.15" style="fill:#fff;opacity:0.2"/></g></svg>

After

Width:  |  Height:  |  Size: 777 B

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="792.1px" height="181.4px" viewBox="0 0 792.1 181.4" style="enable-background:new 0 0 792.1 181.4;" xml:space="preserve"
>
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:none;}
.st2{opacity:0.46;fill:#FFFFFF;enable-background:new ;}
.st3{opacity:0.35;fill:#FFFFFF;enable-background:new ;}
.st4{opacity:0.2;fill:#FFFFFF;enable-background:new ;}
</style>
<g id="Ebene_2_1_">
<path class="st0" d="M403.7,82.6c-1.8,0-1.8,0.2-1.8,2.1v1.2h3.4v1.5h-3.4v8.5h-2.1v-8.5h-1.9v-1.5h1.9v-1.2c0-2.3,0.5-3.6,3.1-3.6
c1,0,2,0.1,3,0.4v1.2C405.1,82.7,404.4,82.6,403.7,82.6z"/>
<path class="st0" d="M411.9,87.1c-0.7,0-1.3,0-2,0.1v8.6h-2.1v-9.8c2.1-0.6,4.3-0.6,6.4,0v1.2C413.5,87.2,412.7,87.1,411.9,87.1z"
/>
<path class="st0" d="M420,96.1c-4.1,0-4.5-0.7-4.5-5.1c0-4.6,0.6-5.3,4.5-5.3c3.9,0,4.5,0.7,4.5,5.3
C424.4,95.5,424.1,96.1,420,96.1z M422.3,90.9c0-3.8-0.2-3.8-2.2-3.8h-0.1c-2.1,0-2.3,0-2.3,3.8v0.2c0,3.6,0.1,3.6,2.3,3.6h0.1
c2.2,0,2.2,0,2.2-3.6V90.9z"/>
<path class="st0" d="M439.6,95.9v-6.7c0-2.1-0.7-2.1-2-2.1c-0.8,0-1.5,0.1-2.3,0.2c0.3,0.6,0.4,1.2,0.3,1.9v6.7h-2.1v-6.7
c0-2.1-0.7-2.1-2-2.1c-0.7,0-1.3,0-2,0.1v8.6h-2.1v-9.8c1.3-0.3,2.7-0.4,4.1-0.4c0.9,0,1.8,0.1,2.6,0.4c1.1-0.3,2.3-0.4,3.4-0.4
c2.6,0,4.1,0.2,4.1,3.6v6.7H439.6z"/>
<path class="st0" d="M455,96.1c-2.6,0-3.1-1.2-3.1-3.6v-5.2H450v-1.5h1.9v-3.4h2.1v3.4h3.4v1.5H454v5.2c0,1.8,0,2.1,1.8,2.1
c0.7,0,1.5-0.1,2.2-0.2v1.2C457.1,96,456,96.2,455,96.1z"/>
<path class="st0" d="M462.4,91.5c0,3.2,0.1,3.3,2.5,3.3c1.3,0,2.7-0.1,4-0.2v1.2c-1.4,0.3-2.9,0.4-4.3,0.4c-3.9,0-4.3-0.7-4.3-5.1
c0-4.6,0.6-5.3,4.5-5.3s4.5,0.7,4.5,5.3v0.5H462.4z M464.8,87.1c-2.1,0-2.3,0-2.3,3.1h4.7C467.1,87.1,466.9,87.1,464.8,87.1z"/>
<path class="st0" d="M478.7,95.9l-2.5-4.3l-2.5,4.3h-2.2l3-5.1l-2.8-4.9h2.3l2.4,4.1l2.4-4.1h2.2l-2.8,4.9l3,5.1H478.7z"/>
<path class="st0" d="M487.6,96.1c-2.6,0-3.1-1.2-3.1-3.6v-5.2h-1.9v-1.5h1.9v-3.4h2.1v3.4h3.4v1.5h-3.4v5.2c0,1.8,0,2.1,1.8,2.1
c0.7,0,1.5-0.1,2.2-0.2v1.2C489.6,96,488.6,96.2,487.6,96.1z"/>
<path class="st0" d="M498.8,95.3v-1.6l7.3-3.7l-7.3-3.7v-1.5l9.1,4.6v1.5L498.8,95.3z"/>
<path class="st0" d="M520.9,96.1c-2.6,0-3.1-1.2-3.1-3.6v-5.2h-1.9v-1.5h1.9v-3.4h2.1v3.4h3.4v1.5h-3.4v5.2c0,1.8,0,2.1,1.8,2.1
c0.7,0,1.5-0.1,2.2-0.2v1.2C522.9,96,521.9,96.2,520.9,96.1z"/>
<path class="st0" d="M530.6,96.1c-4.1,0-4.5-0.7-4.5-5.1c0-4.6,0.6-5.3,4.5-5.3c3.9,0,4.5,0.7,4.5,5.3
C535.1,95.5,534.8,96.1,530.6,96.1z M533,90.9c0-3.8-0.2-3.8-2.2-3.8h-0.1c-2.1,0-2.3,0-2.3,3.8v0.2c0,3.6,0.1,3.6,2.3,3.6h0.1
c2.2,0,2.2,0,2.2-3.6V90.9z"/>
<path class="st0" d="M548.2,96.1c-4,0-4.4-0.7-4.4-5.1c0-4.6,0.6-5.3,4.4-5.3c0.8,0,1.5,0.1,2.3,0.2v-4.5h2.1v14.3
C551.1,96,549.6,96.2,548.2,96.1z M550.5,87.2c-0.7-0.1-1.5-0.1-2.2-0.1c-2.2,0-2.3,0-2.3,3.8v0.2c0,3.6,0.1,3.6,2.3,3.6
c0.7,0,1.5,0,2.2-0.2V87.2z"/>
<path class="st0" d="M560,96.1c-2.3,0-4.4,0-4.4-3.2c0-3,1.5-3,3.6-3h3v-0.7c0-2.1-0.6-2.2-2.4-2.2c-1.3,0-2.5,0.1-3.8,0.2v-1.2
c1.4-0.3,2.7-0.4,4.1-0.4c2.6,0,4.1,0.2,4.1,3.6v6.5C562.9,96,561.5,96.2,560,96.1z M562.3,91.2h-2.6c-1.5,0-2,0-2,1.7
s0.7,1.8,2.3,1.8c0.7,0,1.5,0,2.2-0.1L562.3,91.2z"/>
<path class="st0" d="M571.7,96.1c-2.6,0-3.1-1.2-3.1-3.6v-5.2h-1.9v-1.5h1.9v-3.4h2.1v3.4h3.4v1.5h-3.4v5.2c0,1.8,0,2.1,1.8,2.1
c0.7,0,1.5-0.1,2.2-0.2v1.2C573.8,96,572.7,96.2,571.7,96.1z"/>
<path class="st0" d="M581.2,96.1c-2.3,0-4.4,0-4.4-3.2c0-3,1.5-3,3.6-3h3v-0.7c0-2.1-0.6-2.2-2.4-2.2c-1.3,0-2.5,0.1-3.8,0.2v-1.2
c1.4-0.3,2.7-0.4,4.1-0.4c2.6,0,4.1,0.2,4.1,3.6v6.5C584.1,96,582.6,96.2,581.2,96.1z M583.4,91.2h-2.6c-1.5,0-2,0-2,1.7
s0.7,1.8,2.3,1.8c0.7,0,1.5,0,2.2-0.1L583.4,91.2z"/>
<path class="st0" d="M594.4,95.3v-1.6l7.3-3.7l-7.3-3.7v-1.5l9.1,4.6v1.5L594.4,95.3z"/>
<path class="st0" d="M616.5,96.1c-2.6,0-3.1-1.2-3.1-3.6v-5.2h-1.9v-1.5h1.9v-3.4h2.1v3.4h3.4v1.5h-3.4v5.2c0,1.8,0,2.1,1.8,2.1
c0.7,0,1.5-0.1,2.2-0.2v1.2C618.5,96,617.5,96.2,616.5,96.1z"/>
<path class="st0" d="M626.2,96.1c-4.1,0-4.5-0.7-4.5-5.1c0-4.6,0.6-5.3,4.5-5.3c3.9,0,4.5,0.7,4.5,5.3
C630.7,95.5,630.4,96.1,626.2,96.1z M628.6,90.9c0-3.8-0.2-3.8-2.2-3.8h-0.1c-2.1,0-2.3,0-2.3,3.8v0.2c0,3.6,0.1,3.6,2.3,3.6h0.1
c2.2,0,2.2,0,2.2-3.6V90.9z"/>
<path class="st0" d="M643.8,96.1c-2.3,0-4.4,0-4.4-3.2c0-3,1.5-3,3.6-3h3v-0.7c0-2.1-0.6-2.2-2.4-2.2c-1.3,0-2.5,0.1-3.8,0.2v-1.2
c1.4-0.3,2.7-0.4,4.1-0.4c2.6,0,4.1,0.2,4.1,3.6v6.5C646.7,96,645.3,96.2,643.8,96.1z M646.1,91.2h-2.6c-1.5,0-2,0-2,1.7
s0.7,1.8,2.3,1.8c0.7,0,1.5,0,2.2-0.1L646.1,91.2z"/>
<path class="st0" d="M657.9,95.9v-6.7c0-2.1-0.6-2.1-2.2-2.1c-0.7,0-1.5,0-2.2,0.1v8.6h-2.1v-9.8c1.4-0.3,2.8-0.4,4.3-0.4
c2.8,0,4.3,0.2,4.3,3.6v6.7H657.9z"/>
<path class="st0" d="M667.1,96.1c-2.3,0-4.4,0-4.4-3.2c0-3,1.5-3,3.6-3h3v-0.7c0-2.1-0.6-2.2-2.4-2.2c-1.3,0-2.5,0.1-3.8,0.2v-1.2
c1.4-0.3,2.7-0.4,4.1-0.4c2.6,0,4.1,0.2,4.1,3.6v6.5C670,96,668.6,96.2,667.1,96.1z M669.4,91.2h-2.6c-1.5,0-2,0-2,1.7
s0.7,1.8,2.3,1.8c0.7,0,1.5,0,2.2-0.1L669.4,91.2z"/>
<path class="st0" d="M674.9,95.9V81.4h2.1v14.5H674.9z"/>
<path class="st0" d="M685.3,97.4c-0.9,2.7-1.4,3.3-3,3.3c-1,0-2.1-0.1-3.1-0.4V99c0.8,0.1,1.5,0.2,2.3,0.2c1.1,0,1.2-0.1,1.8-2.1
l0.4-1.2h-1.2l-3.2-10h2.1l2.7,8.5l2.6-8.5h2.2L685.3,97.4z"/>
<path class="st0" d="M695,96.1c-1.4,0-2.8-0.1-4.1-0.4v-1.2c1.1,0.1,2.2,0.2,3.3,0.2c2.2,0,2.7,0,2.7-1.6c0-1.2,0-1.4-1.1-1.6
l-2.4-0.3c-2.5-0.4-2.5-1.4-2.5-2.9c0-2.5,1.8-2.8,3.8-2.8c1.3,0,2.6,0.1,3.8,0.4v1.2c-1-0.1-2-0.2-3-0.2c-2,0-2.5,0-2.5,1.4
c0,1.1,0,1.2,1.1,1.4l2.4,0.4c2.5,0.4,2.5,1.3,2.5,3C699,95.9,697.8,96.1,695,96.1z"/>
<path class="st0" d="M701.9,84v-2.6h2.1V84H701.9z M701.9,95.9v-10h2.1v10H701.9z"/>
<path class="st0" d="M711,96.1c-1.4,0-2.8-0.1-4.1-0.4v-1.2c1.1,0.1,2.2,0.2,3.3,0.2c2.2,0,2.7,0,2.7-1.6c0-1.2,0-1.4-1.1-1.6
l-2.4-0.3c-2.5-0.4-2.5-1.4-2.5-2.9c0-2.5,1.8-2.8,3.8-2.8c1.3,0,2.6,0.1,3.8,0.4v1.2c-1-0.1-2-0.2-3-0.2c-2,0-2.5,0-2.5,1.4
c0,1.1,0,1.2,1.1,1.4l2.4,0.4c2.5,0.4,2.5,1.3,2.5,3C715,95.9,713.8,96.1,711,96.1z"/>
<path class="st0" d="M362.6,101.8V76.1h2.1v25.7H362.6z"/>
</g>
<rect x="58.6" y="56.7" class="st1" width="283.5" height="59.7"/>
<g id="LogotypesRZ">
<path class="st0" d="M161.5,82.8c0-5.4-3.3-8.3-9.5-8.3c-4,0.1-7.9,0.9-11.6,2.3c-0.5,0.2-0.9,0.7-0.9,1.3v19.3h4
c0.7,0,1.2-0.6,1.3-1.2V79.5l0.3-0.1c2.1-0.6,4.2-0.9,6.4-0.8c4.1,0,4.8,1.9,4.8,5.8v13h4c0.7,0,1.2-0.6,1.2-1.2L161.5,82.8z"/>
<path class="st0" d="M178.9,74.5c-7.3,0-12.6,5-12.6,11.8c0,7,5.1,11.7,12.6,11.7c7.3,0,12.5-4.9,12.5-11.7
C191.4,79.2,186.4,74.5,178.9,74.5z M178.9,94c-4.3,0-7.2-3.1-7.2-7.7c0-4.5,2.9-7.7,7.2-7.7c4.3,0,7.2,3.2,7.2,7.7
C186.1,90.9,183.1,94,178.9,94L178.9,94z"/>
<path class="st0" d="M208.1,74.5c-3.7,0.1-7.3,0.8-10.8,2.1c-0.5,0.2-0.9,0.7-0.9,1.2V108h0.2l3.9-0.2c0.7,0,1.2-0.6,1.2-1.2
c0,0,0,0,0,0v-9.3l0.4,0.1c1.8,0.4,3.5,0.6,5.3,0.6c8.2,0,11.9-6.1,11.9-12.1C219.4,80.3,216.4,74.5,208.1,74.5z M207.1,93.9
c-1.7,0-3.4-0.3-5.1-0.7l-0.3-0.1V79.6l0.3-0.1c1.8-0.5,3.6-0.8,5.4-0.9c4.4,0,6.7,2.4,6.7,7.2C214.1,91.1,211.6,93.9,207.1,93.9
L207.1,93.9z"/>
<path class="st0" d="M234.5,74.5c-2.6,0-5.1,0.3-7.7,0.9c-0.6,0.1-1.1,0.6-1,1.2c0,0,0,0.1,0,0.1l0.4,2.5v0.2h0.2
c2.3-0.6,4.6-0.8,7-0.8c3.5,0,6.2,0.3,6.2,4.7v1.1h-0.4c-1.9-0.3-3.8-0.4-5.7-0.5c-4.8,0-10.4,1.2-10.4,7.1c0,4.8,3.6,7.1,10.9,7.1
c1.9,0,3.8-0.2,5.7-0.5c1.4-0.2,2.9-0.6,4.3-1c0.5-0.2,0.9-0.7,0.9-1.2l0,0v-12C244.9,77.1,241.8,74.5,234.5,74.5z M239.7,93.8
h-0.3c-1.7,0.3-3.4,0.4-5.1,0.4c-5.2,0-5.9-1.8-5.9-3.4c0-2.4,1.9-3.5,5.7-3.5c1.8,0,3.5,0.2,5.2,0.4l0.3,0.1V93.8z"/>
<path class="st0" d="M272.8,77.9v-0.3c0-0.6-0.3-1.1-0.9-1.3l-0.5-0.2c-3-1-6.1-1.6-9.2-1.7c-7.8,0-12.4,4.4-12.4,11.9
c0,8.5,5.9,11.6,11.5,11.6c1.9,0,3.9-0.3,5.7-0.9l0.5-0.1v11h0.2l3.9-0.2c0.7,0,1.2-0.6,1.2-1.3L272.8,77.9z M267.5,93.1l-0.3,0.1
c-1.5,0.4-3.1,0.7-4.7,0.7c-4.9,0-7.4-2.5-7.4-7.5c0-5,2.5-7.8,7.1-7.8c1.7,0,3.3,0.3,4.9,0.7l0.3,0.1L267.5,93.1z"/>
<path class="st0" d="M300.4,76.4c0-0.7-0.6-1.3-1.2-1.3c0,0,0,0,0,0h-4v18.6l-0.3,0.1c-1.5,0.2-3,0.4-4.5,0.4
c-4.2,0-6.7-1.1-6.7-6.6c0-2.8,0.2-6,0.4-9v-0.2c0.1-1,0.1-1.3,0.1-1.9c0.1-0.7-0.3-1.3-1-1.4c-0.1,0-0.2,0-0.3,0H279v0.2
c-0.4,5.4-0.7,9.4-0.7,14c0,6,3.6,8.7,11.4,8.7c3.2-0.1,6.4-0.5,9.6-1.2c0.6-0.1,1-0.6,1-1.2l0,0L300.4,76.4L300.4,76.4z"/>
<path class="st0" d="M328.7,85c0-6.3-4.5-10.5-11.2-10.5c-7.3,0-12.2,4.8-12.2,11.9c0,7.1,5.2,11.6,12.9,11.6
c2.7,0,5.3-0.5,7.9-1.3l0,0c0.5-0.2,0.9-0.7,0.8-1.2v-0.2l-0.4-2.2v-0.2h-0.2c-2.4,0.7-5,1-7.5,1c-4.6,0-7.3-2-8-5.8l-0.1-0.4h16.6
c0.6-0.1,1.1-0.5,1.2-1.1v-0.1C328.7,86,328.7,85.5,328.7,85z M310.7,84.2l0.1-0.4c0.4-3.2,3.2-5.6,6.4-5.3c3.8,0,6.1,2,6.2,5.4
v0.4H310.7z"/>
</g>
<g id="Bildmarke_RZ">
<path class="st2" d="M127.7,86.5c-7.8,7-17.9,11-28.4,11.1c-10.5-0.1-20.6-4.1-28.4-11.1c7.8-7,17.9-11,28.4-11.1
C109.8,75.5,119.9,79.5,127.7,86.5z"/>
<rect x="76.7" y="72.5" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -34.6157 89.4271)" class="st3" width="28" height="28"/>
<rect x="93.9" y="72.5" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -29.5558 101.6511)" class="st3" width="28" height="28"/>
<rect x="91.4" y="78.6" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -32.0816 95.5461)" class="st4" width="15.7" height="15.7"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="401.5609756097561" height="401.5609756097561"><g transform="translate(32.78048780487805 32.78048780487805)"><g fill="white" stroke-width="0.5853658536585366" stroke="white"><rect x="57.36585365853659" y="0" width="8.195121951219512px" height="8.195121951219512px"/><rect x="65.5609756097561" y="0" width="8.195121951219512px" height="8.195121951219512px"/><rect x="90.14634146341464" y="0" width="8.195121951219512px" height="8.195121951219512px"/><rect x="98.34146341463415" y="0" width="8.195121951219512px" height="8.195121951219512px"/><rect x="106.53658536585365" y="0" width="8.195121951219512px" height="8.195121951219512px"/><rect x="114.73170731707317" y="0" width="8.195121951219512px" height="8.195121951219512px"/><rect x="131.1219512195122" y="0" width="8.195121951219512px" height="8.195121951219512px"/><rect x="155.70731707317074" y="0" width="8.195121951219512px" height="8.195121951219512px"/><rect x="163.90243902439025" y="0" width="8.195121951219512px" height="8.195121951219512px"/><rect x="180.2926829268293" y="0" width="8.195121951219512px" height="8.195121951219512px"/><rect x="196.6829268292683" y="0" width="8.195121951219512px" height="8.195121951219512px"/><rect x="204.8780487804878" y="0" width="8.195121951219512px" height="8.195121951219512px"/><rect x="221.26829268292684" y="0" width="8.195121951219512px" height="8.195121951219512px"/><rect x="245.85365853658539" y="0" width="8.195121951219512px" height="8.195121951219512px"/><rect x="270.4390243902439" y="0" width="8.195121951219512px" height="8.195121951219512px"/><rect x="8.195121951219512" y="8.195121951219512" width="8.195121951219512px" height="8.195121951219512px"/><rect x="16.390243902439025" y="8.195121951219512" width="8.195121951219512px" height="8.195121951219512px"/><rect x="24.585365853658537" y="8.195121951219512" width="8.195121951219512px" height="8.195121951219512px"/><rect x="32.78048780487805" y="8.195121951219512" width="8.195121951219512px" height="8.195121951219512px"/><rect x="40.97560975609756" y="8.195121951219512" width="8.195121951219512px" height="8.195121951219512px"/><rect x="57.36585365853659" y="8.195121951219512" width="8.195121951219512px" height="8.195121951219512px"/><rect x="65.5609756097561" y="8.195121951219512" width="8.195121951219512px" height="8.195121951219512px"/><rect x="81.95121951219512" y="8.195121951219512" width="8.195121951219512px" height="8.195121951219512px"/><rect x="90.14634146341464" y="8.195121951219512" width="8.195121951219512px" height="8.195121951219512px"/><rect x="131.1219512195122" y="8.195121951219512" width="8.195121951219512px" height="8.195121951219512px"/><rect x="155.70731707317074" y="8.195121951219512" width="8.195121951219512px" height="8.195121951219512px"/><rect x="180.2926829268293" y="8.195121951219512" width="8.195121951219512px" height="8.195121951219512px"/><rect x="188.4878048780488" y="8.195121951219512" width="8.195121951219512px" height="8.195121951219512px"/><rect x="204.8780487804878" y="8.195121951219512" width="8.195121951219512px" height="8.195121951219512px"/><rect x="221.26829268292684" y="8.195121951219512" width="8.195121951219512px" height="8.195121951219512px"/><rect x="254.0487804878049" y="8.195121951219512" width="8.195121951219512px" height="8.195121951219512px"/><rect x="270.4390243902439" y="8.195121951219512" width="8.195121951219512px" height="8.195121951219512px"/><rect x="286.8292682926829" y="8.195121951219512" width="8.195121951219512px" height="8.195121951219512px"/><rect x="295.0243902439024" y="8.195121951219512" width="8.195121951219512px" height="8.195121951219512px"/><rect x="303.219512195122" y="8.195121951219512" width="8.195121951219512px" height="8.195121951219512px"/><rect x="311.4146341463415" y="8.195121951219512" width="8.195121951219512px" height="8.195121951219512px"/><rect x="319.609756097561" y="8.195121951219512" width="8.195121951219512px" height="8.195121951219512px"/><rect x="8.195121951219512" y="16.390243902439025" width="8.195121951219512px" height="8.195121951219512px"/><rect x="40.97560975609756" y="16.390243902439025" width="8.195121951219512px" height="8.195121951219512px"/><rect x="57.36585365853659" y="16.390243902439025" width="8.195121951219512px" height="8.195121951219512px"/><rect x="65.5609756097561" y="16.390243902439025" width="8.195121951219512px" height="8.195121951219512px"/><rect x="81.95121951219512" y="16.390243902439025" width="8.195121951219512px" height="8.195121951219512px"/><rect x="114.73170731707317" y="16.390243902439025" width="8.195121951219512px" height="8.195121951219512px"/><rect x="131.1219512195122" y="16.390243902439025" width="8.195121951219512px" height="8.195121951219512px"/><rect x="139.3170731707317" y="16.390243902439025" width="8.195121951219512px" height="8.195121951219512px"/><rect x="147.5121951219512" y="16.390243902439025" width="8.195121951219512px" height="8.195121951219512px"/><rect x="155.70731707317074" y="16.3Line truncated

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

File diff suppressed because it is too large. Load diff
+36
View File
@@ -0,0 +1,36 @@
/* @preserve
* JSONPatch.js
*
* A Dharmafly project written by Thomas Parslow
* <tom@almostobsolete.net> and released with the kind permission of
* NetDev.
*
* Copyright 2011-2013 Thomas Parslow. All rights reserved.
* Permission is hereby granted,y free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*
* Implements the JSON Patch IETF RFC 6902 as specified at:
*
* http://tools.ietf.org/html/rfc6902
*
* Also implements the JSON Pointer IETF RFC 6901 as specified at:
*
* http://tools.ietf.org/html/rfc6901
*
*/
!function(root,factory){"object"==typeof exports?factory(module.exports):"function"==typeof define&&define.amd?define(["exports"],factory):(root.jsonpatch={},root.returnExports=factory(root.jsonpatch))}(this,function(exports){function InvalidPatch(message){Error.call(this,message),this.message=message}function PatchApplyError(message){Error.call(this,message),this.message=message}function clone(o){var cloned,key;if(isArray(o))return o.slice();if(null===o)return o;if("object"==typeof o){cloned={};for(key in o)Object.hasOwnProperty.call(o,key)&&(cloned[key]=o[key]);return cloned}return o}function deepEqual(a,b){var key;if(a===b)return!0;if(typeof a!=typeof b)return!1;if("object"!=typeof a)return!1;var aIsArray=isArray(a),bIsArray=isArray(b);if(aIsArray!==bIsArray)return!1;if(!aIsArray){for(key in a)if(Object.hasOwnProperty(a,key)&&(!Object.hasOwnProperty(b,key)||!deepEqual(a[key],b[key])))return!1;for(key in b)if(Object.hasOwnProperty(b,key)&&!Object.hasOwnProperty(a,key))return!1;return!0}if(a.length!=b.length)return!1;for(var i=0;i<a.length;i++)return deepEqual(a[i],b[i])}function validateOp(operation){var i,required;if(!operation.op)throw new InvalidPatch("Operation missing!");if(!_operationRequired.hasOwnProperty(operation.op))throw new InvalidPatch("Invalid operation!");if(!("path"in operation))throw new InvalidPatch("Path missing!");for(required=_operationRequired[operation.op],i=0;i<required.length;i++)if(!(required[i]in operation))throw new InvalidPatch(operation.op+" must have key "+required[i])}function compileOperation(operation,mutate){validateOp(operation);var op=operation.op,path=new JSONPointer(operation.path),value=operation.value,from=operation.from?new JSONPointer(operation.from):null;switch(op){case"add":return function(doc){return path.add(doc,value,mutate)};case"remove":return function(doc){return path.remove(doc,mutate)};case"replace":return function(doc){return path.replace(doc,value,mutate)};case"move":if(path.subsetOf(from))throw new InvalidPatch("destination must not be a child of source");return function(doc){var value=from.get(doc),intermediate=from.remove(doc,mutate);return path.add(intermediate,value,mutate)};case"copy":return function(doc){var value=from.get(doc);return path.add(doc,value,mutate)};case"test":return function(doc){if(!deepEqual(path.get(doc),value))throw new PatchApplyError("Test operation failed. Value did not match.");return doc}}}var apply_patch,JSONPatch,JSONPointer,_operationRequired,isArray;isArray=Array.isArray||function(obj){return"[object Array]"==Object.prototype.toString.call(obj)},exports.apply_patch=apply_patch=function(doc,patch){return new JSONPatch(patch).apply(doc)},exports.InvalidPatch=InvalidPatch,InvalidPatch.prototype=new Error,exports.PatchApplyError=PatchApplyError,PatchApplyError.prototype=new Error,exports.JSONPointer=JSONPointer=function(pathStr){var i,split,path=[];if(split=pathStr.split("/"),""!==split[0])throw new InvalidPatch("JSONPointer must start with a slash (or be an empty string)!");for(i=1;i<split.length;i++)path[i-1]=split[i].replace(/~1/g,"/").replace(/~0/g,"~");this.path=path,this.length=path.length},JSONPointer.prototype._get_segment=function(index,node){var segment=this.path[index];if(isArray(node))if("-"===segment)segment=node.length;else{if(!segment.match(/^[0-9]*$/))throw new PatchApplyError("Expected a number to segment an array");segment=parseInt(segment,10)}return segment},JSONPointer.prototype._action=function(doc,handler,mutate){function follow_pointer(node,index){var segment,subnode;if(mutate||(node=clone(node)),segment=that._get_segment(index,node),index==that.path.length-1)node=handler(node,segment);else{if(isArray(node)){if(node.length<=segment)throw new PatchApplyError("Path not found in document")}else{if("object"!=typeof node)throw new PatchApplyError("Path not found in document");if(!Object.hasOwnProperty.call(node,segment))throw new PatchApplyError("Path not found in document")}subnode=follow_pointer(node[segment],index+1),mutate||(node[segment]=subnode)}return node}var that=this;return follow_pointer(doc,0)},JSONPointer.prototype.add=function(doc,value,mutate){return 0===this.length?value:this._action(doc,function(node,lastSegment){if(isArray(node)){if(lastSegment>node.length)throw new PatchApplyError("Add operation must not attempt to create a sparse array!");node.splice(lastSegment,0,clone(value))}else node[lastSegment]=clone(value);return node},mutate)},JSONPointer.prototype.remove=function(doc,mutate){return 0===this.length?void 0:this._action(doc,function(node,lastSegment){if(!Object.hasOwnProperty.call(node,lastSegment))throw new PatchApplyError("Remove operation must point to an existing value!");return isArray(node)?node.splice(lastSegment,1):delete node[lastSegment],node},mutate)},JSONPointer.prototype.replace=function(doc,value,mutate){return 0===this.length?value:this._action(doc,function(node,lastSegment){if(!Object.hasOwnProperty.call(node,lastSegment))throw new PatchApplyError("Replace operLine truncated
+2
View File
@@ -0,0 +1,2 @@
var List;List=function(){var t={"./src/add-async.js":function(t){t.exports=function(t){return function e(r,n,s){var i=r.splice(0,50);s=(s=s||[]).concat(t.add(i)),r.length>0?setTimeout((function(){e(r,n,s)}),1):(t.update(),n(s))}}},"./src/filter.js":function(t){t.exports=function(t){return t.handlers.filterStart=t.handlers.filterStart||[],t.handlers.filterComplete=t.handlers.filterComplete||[],function(e){if(t.trigger("filterStart"),t.i=1,t.reset.filter(),void 0===e)t.filtered=!1;else{t.filtered=!0;for(var r=t.items,n=0,s=r.length;n<s;n++){var i=r[n];e(i)?i.filtered=!0:i.filtered=!1}}return t.update(),t.trigger("filterComplete"),t.visibleItems}}},"./src/fuzzy-search.js":function(t,e,r){r("./src/utils/classes.js");var n=r("./src/utils/events.js"),s=r("./src/utils/extend.js"),i=r("./src/utils/to-string.js"),a=r("./src/utils/get-by-class.js"),o=r("./src/utils/fuzzy.js");t.exports=function(t,e){e=s({location:0,distance:100,threshold:.4,multiSearch:!0,searchClass:"fuzzy-search"},e=e||{});var r={search:function(n,s){for(var i=e.multiSearch?n.replace(/ +$/,"").split(/ +/):[n],a=0,o=t.items.length;a<o;a++)r.item(t.items[a],s,i)},item:function(t,e,n){for(var s=!0,i=0;i<n.length;i++){for(var a=!1,o=0,l=e.length;o<l;o++)r.values(t.values(),e[o],n[i])&&(a=!0);a||(s=!1)}t.found=s},values:function(t,r,n){if(t.hasOwnProperty(r)){var s=i(t[r]).toLowerCase();if(o(s,n,e))return!0}return!1}};return n.bind(a(t.listContainer,e.searchClass),"keyup",t.utils.events.debounce((function(e){var n=e.target||e.srcElement;t.search(n.value,r.search)}),t.searchDelay)),function(e,n){t.search(e,n,r.search)}}},"./src/index.js":function(t,e,r){var n=r("./node_modules/string-natural-compare/natural-compare.js"),s=r("./src/utils/get-by-class.js"),i=r("./src/utils/extend.js"),a=r("./src/utils/index-of.js"),o=r("./src/utils/events.js"),l=r("./src/utils/to-string.js"),u=r("./src/utils/classes.js"),c=r("./src/utils/get-attribute.js"),f=r("./src/utils/to-array.js");t.exports=function(t,e,h){var d,v=this,g=r("./src/item.js")(v),m=r("./src/add-async.js")(v),p=r("./src/pagination.js")(v);d={start:function(){v.listClass="list",v.searchClass="search",v.sortClass="sort",v.page=1e4,v.i=1,v.items=[],v.visibleItems=[],v.matchingItems=[],v.searched=!1,v.filtered=!1,v.searchColumns=void 0,v.searchDelay=0,v.handlers={updated:[]},v.valueNames=[],v.utils={getByClass:s,extend:i,indexOf:a,events:o,toString:l,naturalSort:n,classes:u,getAttribute:c,toArray:f},v.utils.extend(v,e),v.listContainer="string"==typeof t?document.getElementById(t):t,v.listContainer&&(v.list=s(v.listContainer,v.listClass,!0),v.parse=r("./src/parse.js")(v),v.templater=r("./src/templater.js")(v),v.search=r("./src/search.js")(v),v.filter=r("./src/filter.js")(v),v.sort=r("./src/sort.js")(v),v.fuzzySearch=r("./src/fuzzy-search.js")(v,e.fuzzySearch),this.handlers(),this.items(),this.pagination(),v.update())},handlers:function(){for(var t in v.handlers)v[t]&&v.handlers.hasOwnProperty(t)&&v.on(t,v[t])},items:function(){v.parse(v.list),void 0!==h&&v.add(h)},pagination:function(){if(void 0!==e.pagination){!0===e.pagination&&(e.pagination=[{}]),void 0===e.pagination[0]&&(e.pagination=[e.pagination]);for(var t=0,r=e.pagination.length;t<r;t++)p(e.pagination[t])}}},this.reIndex=function(){v.items=[],v.visibleItems=[],v.matchingItems=[],v.searched=!1,v.filtered=!1,v.parse(v.list)},this.toJSON=function(){for(var t=[],e=0,r=v.items.length;e<r;e++)t.push(v.items[e].values());return t},this.add=function(t,e){if(0!==t.length){if(!e){var r=[],n=!1;void 0===t[0]&&(t=[t]);for(var s=0,i=t.length;s<i;s++){var a;n=v.items.length>v.page,a=new g(t[s],void 0,n),v.items.push(a),r.push(a)}return v.update(),r}m(t.slice(0),e)}},this.show=function(t,e){return this.i=t,this.page=e,v.update(),v},this.remove=function(t,e,r){for(var n=0,s=0,i=v.items.length;s<i;s++)v.items[s].values()[t]==e&&(v.templater.remove(v.items[s],r),v.items.splice(s,1),i--,s--,n++);return v.update(),n},this.get=function(t,e){for(var r=[],n=0,s=v.items.length;n<s;n++){var i=v.items[n];i.values()[t]==e&&r.push(i)}return r},this.size=function(){return v.items.length},this.clear=function(){return v.templater.clear(),v.items=[],v},this.on=function(t,e){return v.handlers[t].push(e),v},this.off=function(t,e){var r=v.handlers[t],n=a(r,e);return n>-1&&r.splice(n,1),v},this.trigger=function(t){for(var e=v.handlers[t].length;e--;)v.handlers[t][e](v);return v},this.reset={filter:function(){for(var t=v.items,e=t.length;e--;)t[e].filtered=!1;return v},search:function(){for(var t=v.items,e=t.length;e--;)t[e].found=!1;return v}},this.update=function(){var t=v.items,e=t.length;v.visibleItems=[],v.matchingItems=[],v.templater.clear();for(var r=0;r<e;r++)t[r].matching()&&v.matchingItems.length+1>=v.i&&v.visibleItems.length<v.page?(t[r].show(),v.visibleItems.push(t[r]),v.matchingItems.push(t[r])):t[r].matching()?(v.matchingItems.push(t[r]),t[r].hide()):t[r].hide();return v.trigger("updated"),v},d.start()}},"./src/item.js":function(t){t.exports=function(t){return function(e,rLine truncated
//# sourceMappingURL=list.min.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"list.min.js","sources":["webpack://List/list.min.js"],"mappings":"AAAA","sourceRoot":""}
+6
View File
@@ -0,0 +1,6 @@
/*!
* Materialize v1.0.0 (http://materializecss.com)
* Copyright 2014-2017 Materialize
* MIT License (https://raw.githubusercontent.com/Dogfalo/materialize/master/LICENSE)
*/
var _get=function t(e,i,n){null===e&&(e=Function.prototype);var s=Object.getOwnPropertyDescriptor(e,i);if(void 0===s){var o=Object.getPrototypeOf(e);return null===o?void 0:t(o,i,n)}if("value"in s)return s.value;var a=s.get;return void 0!==a?a.call(n):void 0},_createClass=function(){function n(t,e){for(var i=0;i<e.length;i++){var n=e[i];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(t,e,i){return e&&n(t.prototype,e),i&&n(t,i),t}}();function _possibleConstructorReturn(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function _inherits(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}function _classCallCheck(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}window.cash=function(){var i,o=document,a=window,t=Array.prototype,r=t.slice,n=t.filter,s=t.push,e=function(){},h=function(t){return typeof t==typeof e&&t.call},d=function(t){return"string"==typeof t},l=/^#[\w-]*$/,u=/^\.[\w-]*$/,c=/<.+>/,p=/^\w+$/;function v(t,e){e=e||o;var i=u.test(t)?e.getElementsByClassName(t.slice(1)):p.test(t)?e.getElementsByTagName(t):e.querySelectorAll(t);return i}function f(t){if(!i){var e=(i=o.implementation.createHTMLDocument(null)).createElement("base");e.href=o.location.href,i.head.appendChild(e)}return i.body.innerHTML=t,i.body.childNodes}function m(t){"loading"!==o.readyState?t():o.addEventListener("DOMContentLoaded",t)}function g(t,e){if(!t)return this;if(t.cash&&t!==a)return t;var i,n=t,s=0;if(d(t))n=l.test(t)?o.getElementById(t.slice(1)):c.test(t)?f(t):v(t,e);else if(h(t))return m(t),this;if(!n)return this;if(n.nodeType||n===a)this[0]=n,this.length=1;else for(i=this.length=n.length;s<i;s++)this[s]=n[s];return this}function _(t,e){return new g(t,e)}var y=_.fn=_.prototype=g.prototype={cash:!0,length:0,push:s,splice:t.splice,map:t.map,init:g};function k(t,e){for(var i=t.length,n=0;n<i&&!1!==e.call(t[n],t[n],n,t);n++);}function b(t,e){var i=t&&(t.matches||t.webkitMatchesSelector||t.mozMatchesSelector||t.msMatchesSelector||t.oMatchesSelector);return!!i&&i.call(t,e)}function w(e){return d(e)?b:e.cash?function(t){return e.is(t)}:function(t,e){return t===e}}function C(t){return _(r.call(t).filter(function(t,e,i){return i.indexOf(t)===e}))}Object.defineProperty(y,"constructor",{value:_}),_.parseHTML=f,_.noop=e,_.isFunction=h,_.isString=d,_.extend=y.extend=function(t){t=t||{};var e=r.call(arguments),i=e.length,n=1;for(1===e.length&&(t=this,n=0);n<i;n++)if(e[n])for(var s in e[n])e[n].hasOwnProperty(s)&&(t[s]=e[n][s]);return t},_.extend({merge:function(t,e){for(var i=+e.length,n=t.length,s=0;s<i;n++,s++)t[n]=e[s];return t.length=n,t},each:k,matches:b,unique:C,isArray:Array.isArray,isNumeric:function(t){return!isNaN(parseFloat(t))&&isFinite(t)}});var E=_.uid="_cash"+Date.now();function M(t){return t[E]=t[E]||{}}function O(t,e,i){return M(t)[e]=i}function x(t,e){var i=M(t);return void 0===i[e]&&(i[e]=t.dataset?t.dataset[e]:_(t).attr("data-"+e)),i[e]}y.extend({data:function(e,i){if(d(e))return void 0===i?x(this[0],e):this.each(function(t){return O(t,e,i)});for(var t in e)this.data(t,e[t]);return this},removeData:function(s){return this.each(function(t){return i=s,void((n=M(e=t))?delete n[i]:e.dataset?delete e.dataset[i]:_(e).removeAttr("data-"+name));var e,i,n})}});var L=/\S+/g;function T(t){return d(t)&&t.match(L)}function $(t,e){return t.classList?t.classList.contains(e):new RegExp("(^| )"+e+"( |$)","gi").test(t.className)}function B(t,e,i){t.classList?t.classList.add(e):i.indexOf(" "+e+" ")&&(t.className+=" "+e)}function D(t,e){t.classList?t.classList.remove(e):t.className=t.className.replace(e,"")}y.extend({addClass:function(t){var n=T(t);return n?this.each(function(e){var i=" "+e.className+" ";k(n,function(t){B(e,t,i)})}):this},attr:function(e,i){if(e){if(d(e))return void 0===i?this[0]?this[0].getAttribute?this[0].getAttribute(e):this[0][e]:void 0:this.each(function(t){t.setAttribute?t.setAttribute(e,i):t[e]=i});for(var t in e)this.attr(t,e[t]);return this}},hasClass:function(t){var e=!1,i=T(t);return i&&i.length&&this.each(function(t){return!(e=$(t,i[0]))}),e},prop:function(e,i){if(d(e))return void 0===i?this[0][e]:this.each(function(t){t[e]=i});for(var t in e)this.prop(t,e[t]);return this},removeAttr:function(e){return this.each(function(t){t.removeAttribute?t.removeAttribute(e):delete t[e]})},removeClass:function(t){if(!arguments.length)return this.attr("class","");var i=T(t);return i?this.each(function(e){k(i,function(t){D(e,t)})}):this},removeProp:function(e){return this.each(function(t){delete t[e]})},toggleClass:function(t,e){if(void 0!==e)return this[e?"addClassLine truncated
@@ -0,0 +1,282 @@
/**
* This class is used to create a Client object.
* The client handels the client server communication.
* It communicates with the server (e.g. connection or query)
* and recieves data from it, if dynamicMode is true.
* If dynamicMode is false, the client can also handle data that is already
* loaded and is not coming in, in chunks.
*/
class Client {
constructor({corpusId = null,
socket = null,
logging = true,
dynamicMode = true,
fullContext = null} = {}) {
this.corpusId = corpusId;
this.dynamicMode = dynamicMode;
this.logging = logging;
this.requestQueryProgress = 0;
this.socket = socket;
this.eventListeners = {};
this.isBusy = false;
this.fullContext = fullContext;
/**
* Disables all console logging.
* This is global. So every other log message in every other Class or
* function used in conjunction with the client either logs or does not
* log depending on the logging flag. It is kind of hacky but not bad.
* Credits to https://gist.github.com/kmonsoor/0244fdb4ad79a4826371e58a1a5fa984
*/
if (!logging) {
(() => {
let console = (window.console = window.console || {});
[
'assert', 'clear', 'count', 'debug', 'dir', 'dirxml',
'error', 'exception', 'group', 'groupCollapsed', 'groupEnd',
'info', 'log', 'markTimeline', 'profile', 'profileEnd', 'table',
'time', 'timeEnd', 'timeStamp', 'trace', 'warn'
].forEach((method) => {
console[method] = () => {};
});
})();
}
console.info("Client initialized:", this);
}
/**
* Registers one or more event listeners to the Client. Either socket or
* custom javascript events. Event listeners are class instances of
* ClientEventListener implemented further below.
*/
setSocketEventListeners(eventListeners) {
for (let eventListener of eventListeners) {
this.eventListeners[eventListener.type] = eventListener;
}
}
/**
* Loads the event listeners that have been registered with the function
* above so that they will be triggered on their assigned
* type strings. Type strings double as the socket event event names or
* javascript custom event names.
*/
loadSocketEventListeners() {
for (let [type, listener] of Object.entries(this.eventListeners)) {
listener.listenerFunction(type, this);
}
}
/**
* This functions emits the 'notify-view' custom javascript event. This
* triggers specific functions in the View depending on the caseIdentifier.
* The detail object can hold any type of data the View needs to know about
* to represent those to the user.
*/
notifyView(caseIdentifier, detailObject={}, notificationType='info',
raiseModalFeedback=true) {
detailObject.caseIdentifier = caseIdentifier;
detailObject.client = this;
detailObject.notificationType = notificationType;
detailObject.raiseModalFeedback = raiseModalFeedback;
const event = new CustomEvent('notify-view', { detail: detailObject });
console[notificationType]('Client dispatching Notification with details:',
detailObject);
document.dispatchEvent(event);
}
/**
* Connects to the corpus analysis session running on the server side for the
* specified corpus via socket.io.
*/
connect() {
console.info('corpus_analysis_init: Client connecting to session via',
'socket.emit');
this.socket.emit('corpus_analysis_init', this.corpusId);
}
// Gets the meta data of the current corpus.
getMetaData() {
this.isBusy = true;
console.info('corpus_analysis_meta_data: Client getting meta data via',
'socket.emit.');
this.socket.emit('corpus_analysis_meta_data', this.corpusId);
}
/**
* Emits query to the server via socket.io. Server will send the results
* back.
*/
query(queryStr) {
this.isBusy = true;
console.info('corpus_analysis_query: Client sending query via',
'socket.emit for the query', queryStr);
this.socket.emit('corpus_analysis_query', queryStr);
}
/**
* Requests results data either for, 'full-results', 'sub-results' or
* 'inspect-results' (resultsType).
* Gets full results for evere provided dataIndex (one match).
* Full results means with full context. So the Client has to request all
* matches from the server again!
**/
getResultsData(resultsType, dataIndexes, results) {
let tmp_first_cpos = [];
let tmp_last_cpos = [];
let objectKey = '';
if (resultsType === 'full-results') {
objectKey = 'fullResultsData';
} else if (resultsType === 'sub-results') {
objectKey = 'subResultsData';
} else if (resultsType === 'inspect-results') {
objectKey = 'inspectResultsData';
}
// Delete old data before new data is coming in.
results[objectKey].init();
for (let dataIndex of dataIndexes) {
tmp_first_cpos.push(results.data.matches[dataIndex].c[0]);
tmp_last_cpos.push(results.data.matches[dataIndex].c[1]);
}
this.socket.emit('corpus_analysis_get_match_with_full_context',
{type: resultsType,
data_indexes: dataIndexes,
first_cpos: tmp_first_cpos,
last_cpos: tmp_last_cpos,});
}
/**
* Gets results data either for, 'full-results' or 'sub-results'
* Gets results for every provided dataIndex (one match) without full
* context. Because no context is needed the results data is gathered locally
* from results.data and not from the server.
**/
getResultsDataWithoutContext(resultsType, dataIndexes, results, resultsList) {
this.notifyView('results-data-recieving', {fullContext: false});
let objectKey = '';
if (resultsType === 'full-results') {
console.info('Saving full-results data without full context.');
objectKey = 'fullResultsData';
} else if (resultsType === 'sub-results') {
console.info('Saving sub-results data without full context.');
objectKey = 'subResultsData';
}
// Get matches from results.data.
let matches = [];
let cpos = [];
let match;
for (let index of dataIndexes) {
match = results.data.matches[index]
matches.push(match)
// Get cpos from match.
let {lc, c, rc} = resultsList.helperCreateCpos(results.data.cpos_ranges,
match);
cpos.push(...lc);
cpos.push(...c);
cpos.push(...rc);
}
// Get cpos_lookups from cposes.
let cpos_lookup = {};
let textIds = new Set;
for (let single_cpos of cpos) {
textIds.add(results.data.cpos_lookup[single_cpos].text);
Object.assign(cpos_lookup, { [single_cpos]: results.data.cpos_lookup[single_cpos]});
}
let text = {};
let text_lookup = {};
for (let id of textIds) {
text[id] = results.data.text_lookup[id];
Object.assign(text_lookup, text);
}
/**
* Save the data from results.dat either in results.fullResultsData or
* results.subResultsData.
*/
results[objectKey].init();
results[objectKey].matches.push(...matches);
results[objectKey].addData(cpos_lookup, "cpos_lookup");
results[objectKey].addData(text_lookup, "text_lookup");
results[objectKey].addData(results.metaData);
results[objectKey].query = results.data.query;
results[objectKey].corpus_type = resultsType;
results[objectKey].match_count = matches.length;
results[objectKey].cpos_ranges = results.data.cpos_ranges;
results[objectKey].fullContext = false;
if (objectKey === 'subResultsData') {
// Remove match_count from texts, because they are useless in sub results
for (let [key, value] of Object.entries(results[objectKey].text_lookup)) {
delete results[objectKey].text_lookup[key].match_count;
}
}
console.info('Results data without context has been saved.', results);
this.isBusy = false;
this.notifyView('results-data-recieved', {type: resultsType,
results: results,
fullContext: false});
}
}
/**
* This class is used to create an event listener listening for socket or
* javascript custom events.
* Input are an identifying type string, the listener function and callbacks
* which will be executed as part of the listener function. The identifying
* type string is also used as the socket event or custom javascript event name
* identifier.
*/
class ClientEventListener {
constructor(type, listenerFunction) {
this.listenerCallbacks = {};
this.listenerFunction = listenerFunction;
this.type = type;
}
// Registers callbacks to this ClientEventListener.
setCallbacks(listenerCallbacks) {
for (let listenerCallback of listenerCallbacks) {
this.listenerCallbacks[listenerCallback.type] = listenerCallback;
}
}
/** Shorthand to execute all registered callbacks with same defaultArgs
* in insertion order.
* NOTE:
* Since ECMAScript 2015, objects do preserve creation order for
* string and Symbol keys. In JavaScript engines that comply with the
* ECMAScript 2015 spec, iterating over an object with only string keys will
* yield the keys in order of insertion.
* So all modern Browsers.
*/
executeCallbacks(defaultArgs) {
for (let [type, listenerCallback] of Object.entries(this.listenerCallbacks)) {
listenerCallback.callbackFunction(...defaultArgs,
...listenerCallback.args);
}
}
// Executes a specific registered callback by provoding a type string.
executeCallback(defaultArgs, type) {
let listenerCallback = this.listenerCallbacks[type];
let args = defaultArgs.concat(listenerCallback.args) ;
listenerCallback.callbackFunction(...args);
}
}
/**
* This class is used to create an ListenerCallback which will be registered
* to an ClientEventListener so the listener can invoke the associated
* callback functions.
*/
class ListenerCallback {
constructor(type, callbackFunction, argsList) {
this.args = argsList;
this.callbackFunction = callbackFunction;
this.type = type;
}
}
// Export Classes from this module.
export {
Client,
ClientEventListener,
ListenerCallback,
};
@@ -0,0 +1,161 @@
/**
* This callback is called on a socket.on "corpus_analysis_send_meta_data".
* Handels incoming corpus metadata
*/
function saveMetaData(...args) {
let [payload, client, results, rest] = args;
client.notifyView('meta-data-recieving');
results.metaData.init(payload)
console.info('Metada saved:', results);
client.isBusy = false;
client.notifyView('meta-data-recieved');
}
/**
* This callback should be registered to the SocketEventListener 'recieveQueryStatus'.
* It just gets the incoming status data of the issued query
* and does some preperation work like hiding or showing elements and deleting
* the data from the last query.
*/
function prepareQueryData(...args) {
// deletes old data from query issued before this new query
let [payload, client, results, rest] = args;
// always initialize the results to delete data from the query issued before
results.init();
results.data.match_count = payload.match_count;
client.requestQueryProgress = 0;
client.notifyView('query-data-prepareing', { results: results });
}
/**
* This callbacks saves the incoming query data chunks into the model results.
*/
function saveQueryData(...args) {
let [payload, client, results, rest] = args;
// Get data matches length before new chunk data is being inserted
let dataLength = results.data.matches.length;
if (client.dynamicMode) {
// Incorporating new chunk data into full results
results.data.matches.push(...payload.chunk.matches);
results.data.addData(payload.chunk.cpos_lookup, 'cpos_lookup');
results.data.addData(payload.chunk.text_lookup, 'text_lookup');
/**
* Increment match_counts per text in a global results varaible because
* they are coming in chunkwise.
*/
if (payload.chunk.text_lookup) {
for (let [text_key, value] of Object.entries(payload.chunk.text_lookup)) {
if (!(text_key in results.tmp_match_counts)) {
results.tmp_match_counts[text_key] = {match_count: 0};
}
results.tmp_match_counts[text_key].match_count += payload.chunk.text_lookup[text_key].match_count;
}
}
results.data.cpos_ranges = payload.chunk.cpos_ranges;
let queryFormElement = document.querySelector('#query-form');
results.data.getQueryStr(queryFormElement);
client.requestQueryProgress = payload.progress;
client.notifyView('query-data-recieving',
{ results: results,
client: client,
dataLength: dataLength });
console.info('Query data chunk saved', results.data);
if (client.requestQueryProgress === 100) {
client.isBusy = false;
// Update text_lookup with tmp_match_counts.
for (let [text_key, value] of Object.entries(results.tmp_match_counts)) {
results.data.text_lookup[text_key].match_count = results.tmp_match_counts[text_key].match_count;
}
client.notifyView('query-data-recieved');
}
} else {
results.data.matches.push(...payload.matches);
results.data.addData(payload.cpos_lookup, 'cpos_lookup');
results.data.addData(payload.text_lookup, 'text_lookup');
results.data.cpos_ranges = payload.cpos_ranges;
let queryFormElement = document.querySelector('#query-form');
results.data.getQueryStr(queryFormElement);
client.requestQueryProgress = 100;
client.notifyView('query-data-recieving',
{ results: results,
client: client,
dataLength: dataLength });
console.info('Query data chunk saved', results.data);
if (client.requestQueryProgress === 100) {
console.log(results.data);
client.notifyView('query-data-recieved');
}
}
}
/**
* This callback gets the results data for the export. Either requesting it
* whith full context from the server or gets it locally without full context
* from the already present results.data. Result data is identified with the
* dataIndexes. On index is one match.
*/
function getResultsData(...args) {
let [resultsType, dataIndexes, resultsList, client, results, rest] = args;
client.isBusy = true;
if (resultsList.exportFullInspectContext.checked
|| resultsType === 'inspect-results') {
console.info('Get results with full context');
client.getResultsData(resultsType, dataIndexes, results);
} else {
console.info('Get results without full context');
client.getResultsDataWithoutContext(resultsType, dataIndexes, results,
resultsList);
}
}
/**
* Handles incoming results which have been requested via getResultsData(). and
* saves the data accorindgly into the results object.
*/
function saveResultsData(...args) {
let [payload, type, client, results, rest] = args;
let objectKey = '';
if (type === 'full-results') {
console.info('Saving full-results data.');
objectKey = 'fullResultsData';
} else if (type === 'sub-results') {
console.info('Saving sub-results data.');
objectKey = 'subResultsData';
} else if (type = 'inspect-results') {
objectKey = 'inspectResultsData';
console.info('Saving inspect-results data');
}
// Save incoming data. Data is incoming one match at a time.
results[objectKey].matches.push(...payload.matches);
results[objectKey].addData(payload.cpos_lookup, 'cpos_lookup');
results[objectKey].addData(payload.text_lookup, 'text_lookup');
results[objectKey].addData(results.metaData);
results[objectKey].query = results.data.query;
results[objectKey].corpus_type = type;
results[objectKey].match_count += 1;
results[objectKey].cpos_ranges = payload.cpos_ranges;
results[objectKey].fullContext = true;
console.info('Results data has been saved.', results);
// Notify view to update progress bar
client.notifyView('results-data-recieving', {type: type,
progress: payload.progress})
if (payload.progress === 100) {
client.isBusy = false;
if (objectKey === 'fullResultsData') {
// Get match count per text from results.data only for fullResultsData
results[objectKey].text_lookup = results.data.text_lookup;
}
client.notifyView('results-data-recieved', {type: type,
results: results,
fullContext: true});
}
}
// export callbacks
export {
prepareQueryData,
saveMetaData,
saveQueryData,
getResultsData,
saveResultsData,
};
@@ -0,0 +1,201 @@
/**
* This file contains the listener functions which can be assigned to the
* coprus_analysis client. So that the incoming data/status informations will
* be handled. There are several listeners listening for socket .io events.
* Further below one javascript custom event listener is specified. This
* listener listens for javascript custom events which are being dispatched by
* the View (resultsList).
*/
// Listeners for socket io events
/**
* Recieves a corpus analysis connected signal via socket.io.
*/
function recieveConnected(type, client) {
client.socket.on(type, (response) => {
/**
* Check if request for session was OK.
* If OK execute registered callbacks and notify View.
*/
if (response.code === 200) {
console.group('Connected!')
console.info('corpus_analysis_init: Client recieving connected codes',
'codes via socket.on');
console.info(`corpus_analysis_init: ${response.code} - ${response.msg}`);
console.info('corpus_analysis_init: Initialization succeeded');
console.info(response);
client.notifyView('connected');
console.groupEnd();
// get meta data immediately
client.getMetaData();
} else {
let errorText = `Error ${response.code} - ${response.msg}`;
console.group('Connection failed!');
console.error(`corpus_analysis_init: ${errorText}`);
client.notifyView('client-failed', { msg: errorText }, 'error');
console.groupEnd();
}
});
}
/**
* Recieves meta data from the server via socket.io.
*/
function recieveMetaData(type, client) {
client.socket.on(type, (response) => {
/**
* Check if request for session was OK.
* If OK execute registered callbacks and notify View.
*/
if (response.code === 200) {
console.group('Client recieving meta data')
console.info('corpus_analysis_meta_data: Client recieving meta data',
'via socket.on');
console.info(`corpus_analysis_meta_data: ${response.code} - ${response.msg}`);
console.info(response);
// executing the registered callbacks
client.eventListeners[type].executeCallbacks([response.payload]);
console.groupEnd();
} else {
let errorText = `Error ${response.payload.code} - ${response.payload.msg}`;
console.group('Failed to recieve meta data.');
console.error('corpus_analysis_meta_data: Client failed to recieve',
'meta data via socket.on');
console.error(`corpus_analysis_meta_data: ${errorText}`);
client.notifyView('client-failed', { msg: errorText }, 'error');
console.groupEnd();
}
});
}
/**
* Recieves the query process status before any actual results are being
* transmitted. So it recieves error codes if a query failed or
* was invalid etc.
* Also prepares the result.jsList for the incoming data.
*/
function recieveQueryStatus(type, client) {
/**
* Check if request for session was OK.
* If OK execute registered callbacks and notify View.
*/
client.socket.on(type, (response) => {
if (response.code === 200) {
console.group('corpus_analysis_query: Client recieving query process',
'status via socket.on');
console.info(`corpus_analysis_query: ${response.code} - ${response.msg}`);
console.info(response);
// executing the registered callbacks
client.eventListeners[type].executeCallbacks([response.payload]);
console.groupEnd();
} else {
let errorText = `Error ${response.payload.code} - ${response.payload.msg}`;
console.group('corpus_analysis_query: Client failed recieving',
'query process status via socket.on');
if (response.payload.code == 1281) {
errorText += ' - Invalid Query';
console.error(`corpus_analysis_query: ${errorText}`);
client.notifyView('client-failed', { msg: errorText }, 'error', false);
} else {
console.error(`corpus_analysis_query: ${errorText}`);
client.notifyView('client-failed', { msg: errorText }, 'error');
}
console.groupEnd();
}
});
}
/**
* Recieves the query data from the request and handles it.
*/
function recieveQueryData(type, client) {
/**
* Check if request for session was OK.
* If OK execute registered callbacks and notify View.
*/
client.socket.on(type, (response) => {
if (response.code === 200) {
console.group('corpus_analysis_query_results: Recieveing query data')
console.info('Client recieving query data via socket.on');
console.info('Recieved chunk', response);
/**
* Execute registered callbacks and notify View.
*/
client.eventListeners[type].executeCallbacks([response.payload]);
console.info('Added chunk data to results.data.');
console.groupEnd();
} else {
let errorText = `Error ${response.payload.code} - ${response.payload.msg}`;
console.group('corpus_analysis_query_results: Client failed recieving',
'the results via socket.on');
console.error(`corpus_analysis_query: ${errorText}`);
client.notifyView('client-failed', { msg: errorText }, 'error');
console.groupEnd();
}
});
}
/**
* Recieves the data requested by the create Results or sub results button
*/
function recieveResultsData(type, client) {
client.socket.on(type, (response) => {
/**
* Check if request for session was OK.
* If OK execute registered callbacks and notify View.
*/
if (response.code === 200) {
console.group('Client recieving results data')
console.info('corpus_analysis_get_match_with_full_context: Client recieving results data',
'via socket.on');
console.info(`corpus_analysis_get_match_with_full_context: ${response.code} - ${response.msg}`);
console.info(response);
// executing the registered callbacks
client.eventListeners[type].executeCallbacks([response.payload,
response.type]);
console.groupEnd();
} else {
let errorText = `Error ${response.payload.code} - ${response.payload.msg}`;
console.group('Failed to recieve results data.');
console.error('corpus_analysis_get_match_with_full_context: Client failed to recieve',
'results data via socket.on');
console.error(`corpus_analysis_get_match_with_full_context: ${errorText}`);
client.notifyView('client-failed', { msg: errorText }, 'error');
console.groupEnd();
}
});
}
/*
* This is the javascript custom event listener, listening for events
* dispatched by the View.
*/
function recieveViewNotification(type, client) {
document.addEventListener(type, (event) => {
let caseIdentifier = event.detail.caseIdentifier;
switch(caseIdentifier) {
case 'get-results':
console.info('Client getting full results for export.');
// execute callback or functions
client.eventListeners[type].executeCallback([event.detail.resultsType,
event.detail.dataIndexes,
event.detail.resultsList],
caseIdentifier);
break
default:
console.error('Recieved unkown notification case identifier from View');
// do something to not crash the analysis session?
}
});
}
// export listeners from this module
export {
recieveConnected,
recieveMetaData,
recieveQueryStatus,
recieveQueryData,
recieveViewNotification,
recieveResultsData,
};
@@ -0,0 +1,141 @@
/**
* These classes are implementing the data store of the corpus_analysis
* package. If we follow the idea of the Model View Controller Pattern these
* classes combined in the Results class define the Model.
*/
// Results class bundleing the different data objects.
class Results {
constructor() {
this.data = new Data();
this.metaData = new MetaData();
this.fullResultsData = new Data();
this.subResultsData = new Data();
this.inspectResultsData = new Data();
console.info('Initialized the Results object.');
}
// Reset all the data objects in the results class and thus emptying them.
init() {
this.data.init();
this.metaData.init();
this.fullResultsData.init();
this.subResultsData.init();
this.inspectResultsData.init();
// Temporarly save match counts per text
this.tmp_match_counts = {};
}
}
/**
* Class that defines the actual data objects holding the results data
* requested by the client. Data can be the results of a query, full results
* data for the export, sub results data for the export or inspect results data.
* All kinds are structured the same way.
*/
class Data {
/**
* Sets empty object structure. Also usefull to delete old results.
* MatchCount default is 0.
*/
init(matchCount=0, type="results") {
// List of all c with lc and rc CPOS.
this.matches = [];
/**
* CPOS lookup object. CPOS are the key and value are infos about the CPOS
* like lemma, ner, pos, text ID etc. CPOS from the matches correspond to
* exactly one object in the cpos_lookup.
*/
this.cpos_lookup = {};
/**
* Same like above but for text IDs. One CPOS object always has a text ID
* referencing on text object in the text_lookup. Text ID is the key. Values
* are author, publishing year etc.
*/
this.text_lookup = {};
this.match_count = matchCount;
this.corpus_type = 'results';
this.cpos_ranges = null;
this.query = '';
}
/**
* Function to add json data/object data to this data instance.
* If no key is specified the entire data will be assigned to this data
* instance.
*/
addData(jsonData, key=null) {
if (key !== null) {
Object.assign(this[key], jsonData);
} else if (key === null) {
Object.assign(this, jsonData)
}
}
// Get query as a string from the form Element.
getQueryStr(queryFormElement) {
// gets query
let queryFormData;
let queryStr;
queryFormData = new FormData(queryFormElement);
queryStr = queryFormData.get('query-form-query');
this['query'] = queryStr;
}
// Function creates a unique and safe filename for the download.
createDownloadFilename(suffix) {
let today = new Date();
let currentDate = `${today.getUTCFullYear()}` +
`-${(today.getUTCMonth() + 1)}` +
`-${today.getUTCDate()}`;
let currentTime = `${today.getUTCHours()}h` +
`${today.getUTCMinutes()}m` +
`${today.getUTCSeconds()}s`;
let safeFilename = this.query.replace(/[^a-z0-9_-]/gi, "_");
let resultFilename = `UTC-${currentDate}_${currentTime}_${safeFilename}_${suffix}`;
return resultFilename
}
/**
* Function to download data as Blob created from string.
* Should be private but that is not yet a feature of javascript 08.04.2020
*/
download(downloadElement, dataStr, filename, type, filenameSlug) {
filename += filenameSlug;
let file = new Blob([dataStr], {type: type});
var url = URL.createObjectURL(file);
downloadElement.href = url;
downloadElement.download = filename;
}
// Function to download the results as JSON.
downloadJSONRessource(resultFilename, downloadData, downloadElement) {
/**
* Stringify JSON object for json download.
* Use tabs to save some space.
*/
let dataStr = JSON.stringify(downloadData, undefined, "\t");
// Start actual download
this.download(downloadElement, dataStr, resultFilename, "text/json", ".json")
}
}
/**
* Similar to the data class but just intended for meta data about the current
* corpus the client is working with.
*/
class MetaData {
// Sets empty object structure when no input is given.
// if json object like input is given class fields are created from this
init(json={}) {
Object.assign(this, json);
}
}
// Export the classes
export {
Results,
Data,
MetaData
};
@@ -0,0 +1,837 @@
/**
* This class implements a ViewEventListener that is listening for the
* specified
*/
class ViewEventListener {
constructor(type, listenerFunction, args=[]) {
this.listenerFunction = listenerFunction;
this.type = type;
this.args = args;
}
}
/**
* This class is implements a View which handles the reprensentation of the
* data that has been fetched by the Client of the corpus_analysis. This view
* only handles how the data is shown to the user. View extends the list.js
* List class.
*/
class ResultsList extends List {
/**
* If no options are given when a new instance of this class is created
* the options below are used.
*/
static options = {
page: 30,
pagination: [{
name: "paginationTop",
paginationClass: "paginationTop",
innerWindow: 8,
outerWindow: 1
}, {
paginationClass: "paginationBottom",
innerWindow: 8,
outerWindow: 1
}],
valueNames: ["titles", "lc", "c", "rc", {data: ["index"]}],
item: `<span></span>`
};
constructor(idOrElement, options) {
super(idOrElement, options);
/**
* All span tokens which are holding events if expert
* mode is on. Collected here to delete later on.
*/
this.eventTokens = {};
/**
* All token elements which have added classes like chip and hoverable for
* expert view. Collected here to delete later on.
*/
this.currentExpertTokenElements = {};
/**
* Holds True/false for check buttons used to add matches to sub-results.
* If checked, it is True. If unchecked, it is false. Buttons for this
* have the class add. The ittle round check buttons to add matches to sub
* results. Key is match index. Value is true or false as mentioned above.
*/
this.subResultsIndexes = {};
// ViewEventListeners listening for client notifications.
this.notificationListeners = {};
this.knownHTMLElements = new Set();
}
/**
* Function to clear/reset some class field values. Usefull if a new query
* hase been issued by the user.
*/
resetFields() {
this.subResultsIndexes = {};
}
/**
* Function that takes one or more query selector
* strings in an array as an input. The function then creates a
* class field in the ResultsList object with the query selector
* string as the key. The selector will be converted to a valid JavaScript
* Field name i. e. #html-id-string -> this.htmlIdString
* The value will be the identifed element or elements fetched with the
* querySelector respectively querySelectorAll method.
* If the query selector is passed as an Array of length 2, with the second
* element defining modal options, teh identified element will be initialized
* as a modal with the given options.
*/
getHTMLElements(arrayOfSelectors) {
for (let selector of arrayOfSelectors) {
// Check if identified Element should be initialized as a modal.
let modalInit = false;
let options;
if (Array.isArray(selector)) {
options = selector[1];
selector = selector[0];
modalInit = true;
}
// Check if the current selector has already been used.
if (this.knownHTMLElements.has(selector)) {
continue;
} else {
// Get element or elements.
let element;
let elements;
if (selector.startsWith('#')) {
element = document.querySelector(selector);
} else {
elements = document.querySelectorAll(selector);
elements = [...elements];
}
// Create valid javascript instance field name.
let cleanKey = [];
selector = selector.replace(/_/g, '-');
selector.match(/\w+/g).forEach((word) => {
let tmp = word[0].toUpperCase() + word.slice(1);
cleanKey.push(tmp);
});
cleanKey[0] = cleanKey[0].toLowerCase();
cleanKey = cleanKey.join('');
this[cleanKey] = element ? element: elements;
// Initialize current element as modal if modalInit true.
if (modalInit) {
this[cleanKey] = M.Modal.init(this[cleanKey], options);
}
}
// Add current selector to knwonHTMLElements.
this.knownHTMLElements.add(selector);
}
}
/**
* Register ViewEventListeners to the ResultsList. Which will listen for
* the specified event.
*/
setViewEventListeners(notificationListeners) {
for (let notificationListener of notificationListeners) {
this.notificationListeners[notificationListener.type] = notificationListener;
}
}
/**
* Loads the ViewEventListeners so that hey will be listening to their
* assigned custom events.
*/
loadViewEventListeners() {
for (let [type, listener] of Object.entries(this.notificationListeners)) {
if (listener.args.length > 0) {
listener.listenerFunction(...listener.args);
} else {
listener.listenerFunction(type, this);
}
}
}
/**
* This functions sends events to the Client to trigger specific functions to
* trigger new data requests from the server.
*/
notifyClient(caseIdentifier, detailObject={}) {
detailObject.caseIdentifier = caseIdentifier;
const event = new CustomEvent('notify-client', { detail: detailObject });
console.info('Client dispatching Notification:', caseIdentifier);
document.dispatchEvent(event);
}
/**
* Creates cpos either from ranges or not.
*/
helperCreateCpos(cpos_values) {
let lc, c, rc;
/**
* Python range like function from MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from#Sequence_generator_(range)
*/
const range = (start, stop, step) => Array.from({ length: (stop - start) / step + 1}, (_, i) => start + (i * step));
lc = cpos_values.lc ? range(cpos_values.lc[0], cpos_values.lc[1], 1) : [];
c = range(cpos_values.c[0], cpos_values.c[1], 1);
rc = cpos_values.rc ? range(cpos_values.rc[0], cpos_values.rc[1], 1) : [];
return {lc: lc, c: c, rc: rc};
}
// Get display options from display options form element.
static getDisplayOptions(htmlId) {
// gets display options parameters
let displayOptionsFormElement = document.getElementById(htmlId);
let displayOptionsFormData = new FormData(displayOptionsFormElement);
let displayOptionsData =
{
"resultsPerPage": displayOptionsFormData.get("display-options-form-results_per_page"),
"resultsContex": displayOptionsFormData.get("display-options-form-result_context"),
"expertMode": displayOptionsFormData.get("display-options-form-expert_mode")
};
return displayOptionsData
}
/**
* Used in addToSubResults and inspect to toggle the design of the check
* buttons according to its checked unchecked status.
*/
helperActivateAddBtn(btn) {
btn.classList.remove("corpus-analysis-color.lighten");
btn.classList.add("green");
btn.textContent = "check";
}
/**
* Used in addToSubResults and inspect to toggle the design of the check
* buttons according to its checked unchecked status.
*/
helperDeactivateAddBtn(btn) {
btn.classList.remove("green");
btn.classList.add("corpus-analysis-color.lighten");
btn.textContent = "add";
}
/**
* This function is invoked when the users adds or removes a match using the
* add-btn (+ button/or green checkmark) to/from sub-results. When the button
* is clicked the function checks if the current dataIndex ID is already
* saved in subResultsIndexes or not. If it is not the dataIndex will be used
* as a key in subResultsIndexes with the value true. If it is already added
* the entry with the key dataIndex will be deleted from subResultsIndexes.
* Visual feedback (green checkmark if a match has been added etc.) is also
* handled on the basis of the information stored in subResultsIndexes.
*/
addToSubResults(dataIndex, client, tableCall=true) {
let toShowArray;
dataIndex = parseInt(dataIndex);
if (!this.subResultsIndexes[dataIndex]
|| this.subResultsIndexes[dataIndex] === undefined) {
// add button is activated because status is false or undefined
this.helperActivateAddBtn(event.target);
this.subResultsIndexes[dataIndex] = true;
toShowArray = Object.keys(this.subResultsIndexes).map(index => parseInt(index));
// Add 1 because indexes are zero based. User sees 1 based numbering.
toShowArray = toShowArray.map(index => index + 1);
// Allways sort the shown indexes for the user if new match is added.
toShowArray = toShowArray.sort(function(a, b){return a-b});
this.subResultsIndexesDisplay.textContent = toShowArray.join(', ');
M.textareaAutoResize(this.subResultsIndexesDisplay);
this.nrMarkedMatches.textContent = Object.keys(this.subResultsIndexes).length;
} else if (this.subResultsIndexes[dataIndex]) {
// add button is deactivated because status is true
this.helperDeactivateAddBtn(event.target);
delete this.subResultsIndexes[dataIndex];
toShowArray = Object.keys(this.subResultsIndexes).map(index => parseInt(index));
// Add 1 because indexes are zero based. User sees 1 based numbering.
toShowArray = toShowArray.map(index => index + 1);
// Allways sort the shown indexes for the user if new match is added.
toShowArray = toShowArray.sort(function(a, b){return a-b});
this.subResultsIndexesDisplay.textContent = toShowArray.join(', ');
this.nrMarkedMatches.textContent = Object.keys(this.subResultsIndexes).length;
M.textareaAutoResize(this.subResultsIndexesDisplay);
}
// Toggles create button according to the number of ids in addToSubResultsIdsToShow
if (Object.keys(this.subResultsIndexes).length > 0 && !client.isBusy) {
this.subResultsCreate.classList.toggle('disabled', false);
} else if (Object.keys(this.subResultsIndexes).length === 0) {
this.subResultsCreate.classList.toggle('disabled', true);
}
/**
* After a match as been added or removed the export button will be
* hidden because the sub-results have been altered and have to be built
* again. Thus subResultsCreateElement has to be shown again.
*/
this.subResultsExport.classList.add("hide");
this.subResultsCreate.classList.remove("hide");
/**
* Also activate/deactivate buttons in the table/resultsList accordingly
* if button in inspect was activated/deactivated.
* This part only runs if tableCall is set to false when this function is
* called.
*/
if (!tableCall) {
this.getHTMLElements(['#query-results-table']);
let container = this.queryResultsTable.querySelector(`[data-index="${dataIndex}"]`);
let tableAddBtn = container.querySelector('.add-btn'); // gets the add button from the list view
if (this.subResultsIndexes[dataIndex]) {
this.helperActivateAddBtn(tableAddBtn);
} else {
this.helperDeactivateAddBtn(tableAddBtn);
}
}
}
// Toggle inspect buttons depending on the Client status
toggleInspectButtons(client) {
if (!client.isBusy) {
this.activateInspectButtons();
} else if (client.isBusy) {
this.deactivateInspectButtons();
}
}
// Helper function. Should be private if feature is available.
activateInspectButtons() {
let inspectBtnElements;
inspectBtnElements = document.querySelectorAll('.inspect');
for (let inspectBtn of inspectBtnElements) {
inspectBtn.classList.toggle('disabled', false);
}
}
// Helper function. Should be private if feature is available.
deactivateInspectButtons() {
let inspectBtnElements;
inspectBtnElements = document.querySelectorAll('.inspect');
for (let inspectBtn of inspectBtnElements) {
inspectBtn.classList.toggle('disabled', true);
}
}
/**
* Function to inspect one match in more detail (Showing more context).
* If in dynamic mode the view notifies the client to requests the new
* context for the one match identified by the given dataIndex.
* If not in dynamic mode the the needed context will be gathered from the
* already present results in results.data.
*/
inspect(client, results, dataIndex, type) {
// initialize context modal
this.getHTMLElements([
['#context-modal', true],
'#context-results',
'#create-inspect-menu',
'#create-from-inspect',
]);
// Clear fields from old data on every new inspect() call.
this.contextId = dataIndex[0];
this.contextResults.innerHTML = '';
// Open modal.
this.contextModal.open();
this.contextResults.insertAdjacentHTML('afterbegin', `
<div class="progress">
<div class="indeterminate"></div>
</div>
`);
if (client.dynamicMode) {
// Notify Client to get results from server.
this.notifyClient('get-results', {resultsType: 'inspect-results',
dataIndexes: dataIndex,
resultsList: this});
} else {
// Gather results data from already present data.
results.inspectResultsData.matches = [results.data.matches[dataIndex[0]]];
results.inspectResultsData.cpos_ranges = results.data.cpos_ranges;
this.showMatchContext(results, client)
}
// Match nr for user to display derived from data_index.
let contextMatchNrElement = document.getElementById("context-match-nr");
contextMatchNrElement.textContent = this.contextId + 1;
// Add the add button to add this match to sub results with onclick event.
let classes = `btn-floating btn waves-effect` +
` waves-light corpus-analysis-color.lighten right`
let addToSubResultsIdsBtn = document.createElement("a");
addToSubResultsIdsBtn.setAttribute("class", classes + ` add`);
addToSubResultsIdsBtn.innerHTML = '<i class="material-icons">add</i>';
addToSubResultsIdsBtn.onclick= () => {
this.addToSubResults(dataIndex[0], client, false)
};
/**
* Checks if the match has or has not been added to sub results yet.
* Sets the color and status of the button accordingly.
*/
if (this.subResultsIndexes[dataIndex[0]]) {
this.helperActivateAddBtn(addToSubResultsIdsBtn.firstElementChild);
} else if (!this.subResultsIndexes[dataIndex[0]]) {
this.helperDeactivateAddBtn(addToSubResultsIdsBtn.firstElementChild);
}
this.createInspectMenu.innerHTML = '';
this.createInspectMenu.appendChild(addToSubResultsIdsBtn);
// Hide create menu if not in dynamic mode.
if (!client.dynamicMode) {
this.createFromInspect.classList.add('hide');
}
}
/**
* Create Element from HTML String. Helper function should be private.
* https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro/35385518#35385518
*/
HTMLTStrToElement(htmlStr) {
let template = document.createElement("template");
htmlStr = htmlStr.trim();
template.innerHTML = htmlStr;
return template.content.firstChild;
}
/**
* Used either as a callback if the client has been notified to get new
* results with new full context. Or just directly invoced as a function
* with the according input data.
*/
showMatchContext(results, client) {
this.getHTMLElements([
'#context-results',
'#inspect-display-options-form-expert_mode_inspect',
'#inspect-display-options-form-highlight_sentences',
'#context-sentences'
])
let uniqueS = new Set();
let uniqueContextS = new Set();
let {lc, c, rc} = this.helperCreateCpos(results.inspectResultsData.matches[0]);
// Create sentence strings as tokens.
let tokenHTMLArray = [];
let htmlTokenStr = ``;
let tokenHTMlElement;
let token;
for (let cpos of lc) {
if (client.dynamicMode) {
token = results.inspectResultsData.cpos_lookup[cpos];
// If client is not in dynamic mode use cpos_lookup from results.data
} else {
token = results.data.cpos_lookup[cpos];
}
uniqueS.add(token.s)
htmlTokenStr = `<span class="token"` +
`data-sid="${token.s}"` +
`data-cpos="${cpos}">` +
`${token.word}` +
`</span>`;
tokenHTMlElement = this.HTMLTStrToElement(htmlTokenStr)
tokenHTMLArray.push(tokenHTMlElement);
}
for (let cpos of c) {
if (client.dynamicMode) {
token = results.inspectResultsData.cpos_lookup[cpos];
// If client is not in dynamic mode use cpos_lookup from results.data
} else {
token = results.data.cpos_lookup[cpos];
}
uniqueContextS.add(token.s);
uniqueS.add(token.s);
htmlTokenStr = `<span class="token bold light-green"` +
`data-sid="${token.s}"` +
`data-cpos="${cpos}"` +
`style="text-decoration-line: underline;">` +
`${token.word}` +
`</span>`;
tokenHTMlElement = this.HTMLTStrToElement(htmlTokenStr)
tokenHTMLArray.push(tokenHTMlElement);
}
results.inspectResultsData["context_s_ids"] = Array.from(uniqueContextS);
for (let cpos of rc) {
if (client.dynamicMode) {
token = results.inspectResultsData.cpos_lookup[cpos];
// If client is not in dynamic mode use cpos_lookup from results.data
} else {
token = results.data.cpos_lookup[cpos];
}
uniqueS.add(token.s)
htmlTokenStr = `<span class="token"` +
`data-sid="${token.s}"` +
`data-cpos="${cpos}">` +
`${token.word}` +
`</span>`;
tokenHTMlElement = this.HTMLTStrToElement(htmlTokenStr)
tokenHTMLArray.push(tokenHTMlElement);
}
// Remove loading indeterminate HTML before context is inserted.
this.contextResults.innerHTML = '';
for (let sId of uniqueS) {
let htmlSentence = `<span class="sentence" data-sid="${sId}"></span>`;
let sentenceElement = this.HTMLTStrToElement(htmlSentence);
for (let tokenElement of tokenHTMLArray) {
if (tokenElement.dataset.sid == sId) {
sentenceElement.appendChild(tokenElement);
sentenceElement.insertAdjacentHTML("beforeend", ` `);
} else {
continue;
}
}
this.contextResults.appendChild(sentenceElement);
}
// Add expert mode switch event for the modal to toggle expert mode.
this.inspectDisplayOptionsFormExpertModeInspect.onchange = (event) => {
if (event.target.checked) {
this.expertModeOn("context-results", results);
} else {
this.expertModeOff("context-results")
}
};
// Add switch event to toggle Sentence highlighting.
this.inspectDisplayOptionsFormHighlightSentences.onchange = (event) => {
if (event.target.checked) {
this.higlightContextSentences();
} else {
this.unhighlightContextSentences();
}
};
// Add range event to change nr of context sentences.
this.contextSentences.onchange = (event) => {
this.changeSentenceContext(event.target.value);
}
/**
* Checks on new modal opening if switches are checked
* if switches are checked functions are executed.
*/
if (this.inspectDisplayOptionsFormExpertModeInspect.checked) {
this.expertModeOn("context-results", results);
}
if (this.inspectDisplayOptionsFormHighlightSentences.checked) {
this.higlightContextSentences();
}
/**
* Checks the value of the number of sentences to show on modal opening
* sets context sentences accordingly
*/
this.changeSentenceContext(this.contextSentences.value);
}
// Splits context text into sentences based on spacy sentence split
higlightContextSentences() {
let sentences = document.getElementById("context-results").getElementsByClassName("sentence");
for (let s of sentences) {
s.insertAdjacentHTML("beforeend", `<span><br><br></span>`)
}
}
// Reverse operation of above function.
unhighlightContextSentences() {
let sentences = document.getElementById("context-results").getElementsByClassName("sentence");
let br;
for (let s of sentences) {
br = s.lastChild;
br.remove();
}
}
// Changes how many context sentences in inspect view are shown.
changeSentenceContext(sValue, maxSValue=10) {
sValue = maxSValue - sValue;
// console.log(sValue);
let sentences = document.getElementById("context-results").getElementsByClassName("sentence");
let array = Array.from(sentences);
let toHideArray;
let toShowArray;
if (sValue != 0) {
toHideArray = array.slice(0, sValue).concat(array.slice(-(sValue)));
toShowArray = array.slice(sValue, 9).concat(array.slice(9, -(sValue)))
} else {
toHideArray = [];
toShowArray = array;
}
for (let s of toHideArray) {
s.classList.add("hide");
}
for (let s of toShowArray) {
s.classList.remove("hide");
}
}
// ###### Display options functions changing how results are being displayed ######
/**
* Event function that changes the shown hits per page.
* Just alters the resultsList.page property.
*/
changeHitsPerPage(client, results) {
this.page = this.displayOptionsFormResultsPerPage.value;
this.update();
this.changeContext();
this.toggleInspectButtons(client);
if (this.displayOptionsFormExpertMode.checked) {
this.expertModeOn('query-display', results);
}
}
/**
* Event function triggered on context select change also if pagination is
* clicked.
*/
changeContext() {
let newContextValue = this.displayOptionsFormResultContext.value;
let lc = document.querySelectorAll(".left-context");
let rc = document.querySelectorAll(".right-context");
for (let element of lc) {
let arrayLc = Array.from(element.childNodes);
for (let element of arrayLc.reverse().slice(newContextValue)) {
element.classList.add("hide");
}
for (let element of arrayLc.slice(0, newContextValue)) {
element.classList.remove("hide");
}
}
for (let element of rc) {
let arrayRc = Array.from(element.childNodes);
for (let element of arrayRc.slice(newContextValue)) {
element.classList.add("hide");
}
for (let element of arrayRc.slice(0, newContextValue)) {
element.classList.remove("hide");
}
}
}
// ###### Expert view event functions ######
// Function to create a tooltip for the current hovered token.
tooltipEventCreate(event, results) {
let token = results.data.cpos_lookup[event.target.dataset.cpos];
if (!token) {
token = results.inspectResultsData.cpos_lookup[event.target.dataset.cpos];
}
this.currentTooltipElement = M.Tooltip.init(event.target, {
'html': `<table>
<tr>
<th>Token information</th>
<th>Source information</th>
</tr>
<tr>
<td class="left-align">
Word: ${token.word}<br>
Lemma: ${token.lemma}<br>
POS: ${token.pos}<br>
Simple POS: ${token.simple_pos}<br>
NER: ${token.ner}
</td>
<td class="left-align">
Title: ${results.data.text_lookup[token.text].title}
<br>
Author: ${results.data.text_lookup[token.text].author}
<br>
Publishing year: ${results.data.text_lookup[token.text].publishing_year}
</td>
</tr>
</table>`
}
);
}
/**
* Function to destroy the current Tooltip for the current hovered tooltip
* on mouse leave
*/
tooltipEventDestroy(event) {
this.currentTooltipElement.destroy();
}
/**
* Turn the expert mode on for all tokens in the DOM element identified by
* its htmlID.
*/
expertModeOn(htmlId, results) {
if (!Array.isArray(this.currentExpertTokenElements[htmlId])) {
this.currentExpertTokenElements[htmlId] = [];
}
let container = document.getElementById(htmlId);
let tokens = container.querySelectorAll('span.token');
this.currentExpertTokenElements[htmlId].push(...tokens);
this.eventTokens[htmlId] = [];
for (let tokenElement of this.currentExpertTokenElements[htmlId]) {
tokenElement.classList.add('chip', 'hoverable', 'expert-view');
const eventCreate = (event, arg) => this.tooltipEventCreate(event, arg);
tokenElement.onmouseover = (event) => eventCreate(event, results);
tokenElement.onmouseout = (event) => this.tooltipEventDestroy(event);
this.eventTokens[htmlId].push(tokenElement);
}
}
/**
* Turn the expert mode off for all tokens in the DOM element identified by
* its htmlID.
*/
expertModeOff(htmlId) {
if (!Array.isArray(this.currentExpertTokenElements[htmlId])) {
this.currentExpertTokenElements[htmlId] = [];
}
if (!Array.isArray(this.eventTokens[htmlId])) {
this.eventTokens[htmlId] = [];
}
for (let tokenElement of this.currentExpertTokenElements[htmlId]) {
tokenElement.classList.remove("chip", "hoverable", "expert-view");
}
this.currentExpertTokenElements[htmlId] = [];
for (let eventToken of this.eventTokens[htmlId]) {
eventToken.onmouseover = '';
eventToken.onmouseout = '';
}
this.eventTokens[htmlId] = [];
}
createResultRowElement(item, chunk, client, imported=false) {
// Gather values from item.
let values = item.values();
let {lc, c, rc} = this.helperCreateCpos(values)
// Get infos for full match row.
let matchRowElement = document.createElement("tr");
matchRowElement.setAttribute("data-index", values.index)
let lcCellElement = document.createElement("td");
lcCellElement.classList.add("left-context");
matchRowElement.appendChild(lcCellElement);
for (let cpos of lc) {
let token = chunk.cpos_lookup[cpos];
lcCellElement.insertAdjacentHTML("beforeend",
`<span class="token" data-cpos="${cpos}">${token.word} </span>`);
}
// Get infos for hit of match and set actions.
let textTitles = new Set();
let aCellElement = document.createElement("td");
aCellElement.classList.add("actions");
let cCellElement = document.createElement("td");
cCellElement.classList.add("match-hit");
let textTitlesCellElement = document.createElement("td");
textTitlesCellElement.classList.add("titles");
let matchNrElement = document.createElement("td");
matchNrElement.classList.add("match-nr");
matchRowElement.appendChild(cCellElement);
matchRowElement.appendChild(aCellElement);
for (let cpos of c) {
let token = chunk.cpos_lookup[cpos];
cCellElement.insertAdjacentHTML("beforeend",
`<span class="token" data-cpos="${cpos}">${token.word} </span>`);
// Get text titles of every hit cpos token.
textTitles.add(chunk.text_lookup[token.text].title);
}
// Add some interaction buttons.
let css = `margin-right: 5px; margin-bottom: 5px;`
let classes = `btn-floating btn waves-effect` +
` waves-light corpus-analysis-color.lighten`
// Add inspect button to trigger inspect view with more context.
let inspectBtn = document.createElement("a");
inspectBtn.setAttribute("style", css);
inspectBtn.setAttribute("class", classes + ` disabled inspect`
);
inspectBtn.innerHTML = '<i class="material-icons inspect-btn">search</i>';
// Add btn to be able add matches to sub-results.
let addToSubResultsBtn = document.createElement("a");
addToSubResultsBtn.setAttribute("style", css);
addToSubResultsBtn.setAttribute("class", classes + ` add`);
addToSubResultsBtn.innerHTML = '<i class="material-icons add-btn">add</i>';
if (client.dynamicMode || client.fullContext) {
aCellElement.appendChild(inspectBtn);
}
if (client.dynamicMode) {
aCellElement.appendChild(addToSubResultsBtn);
}
// Add text titles at front as first td of one row.
textTitlesCellElement.textContent = [...textTitles].join(", ");
matchRowElement.insertAdjacentHTML("afterbegin", textTitlesCellElement.outerHTML);
matchNrElement.textContent = values.index + 1;
matchRowElement.insertAdjacentHTML("afterbegin", matchNrElement.outerHTML);
// Get infos for right context of match
let rcCellElement = document.createElement("td");
rcCellElement.classList.add("right-context");
matchRowElement.appendChild(rcCellElement);
for (let cpos of rc) {
let token = chunk.cpos_lookup[cpos];
rcCellElement.insertAdjacentHTML("beforeend",
`<span class="token" data-cpos="${cpos}">${token.word} </span>`);
}
return matchRowElement
}
/**
* Creates the HTML table code for the metadata view in the corpus analysis
* interface
*/
createMetaDataForModal(metaDataObject) {
let html = `<div class="col s12">
<table class="highlight">
<thead>
<tr>
<th>Meta Data Description</th>
<th>Value</th>
</tr>
</thead>
<tbody>`
for (let [outerKey, outerValue] of Object.entries(metaDataObject)) {
// Use more descriptive names.
if (outerKey === 'corpus_all_texts') {
let tmpName = 'All texts in this corpus';
html += `<tr>
<td style="text-transform: uppercase;">${tmpName.replace(/_/g, " ")}
</td>`
} else if (outerKey === 'text_lookup') {
let tmpName = 'Texts where the query resulted in matches'
html += `<tr>
<td style="text-transform: uppercase;">${tmpName.replace(/_/g, " ")}
</td>`
} else {
html += `<tr>
<td style="text-transform: uppercase;">${outerKey.replace(/_/g, " ")}
</td>`
}
if (outerKey === "corpus_all_texts" || outerKey === "text_lookup") {
html += `<td>
<ul class="collapsible">`
for (let [innerKey, innerValue] of Object.entries(outerValue)) {
html += `<li class="text-metadata"
data-metadata-key="${outerKey}"
data-text-key="${innerKey}">
<div class="collapsible-header"
data-metadata-key="${outerKey}"
data-text-key="${innerKey}">
<i class="material-icons"
data-metadata-key="${outerKey}"
data-text-key="${innerKey}">info_outline</i>
${innerValue['author']} - ${innerValue['publishing_year']} -
${innerValue['title']}
</div>
<div class="collapsible-body">
<span>
<ul id="bibliographic-data-${outerKey}-${innerKey}"
style="column-count: 2;">
</ul>
</span>
</div>
</li>`
}
html += `</ul>
</td>`
} else {
html += `<td>${outerValue}</td>`
}
html += `</tr>`
}
html += `</tbody>
</table>`
return html
}
/**
* Creates the text details for the texts shown in the corpus analysis
* metadata modal table.
*/
createTextDetails(metaData) {
let metadataKey = event.target.dataset.metadataKey;
let textKey = event.target.dataset.textKey;
let textData = metaData[metadataKey][textKey];
let bibliographicData = document.querySelector(`#bibliographic-data-${metadataKey}-${textKey}`);
bibliographicData.textContent = '';
for (let [key, value] of Object.entries(textData)) {
bibliographicData.insertAdjacentHTML("afterbegin",
`
<li>
<span style="text-transform: capitalize;">${key}:</span>
${value}
</li>
`
);
}
}
};
// Export classes.
export {
ViewEventListener,
ResultsList
};
@@ -0,0 +1,244 @@
/**
* This file contains all the callbacks triggered by the notificationListener.
* Also general callbacks are defined which are doing some hiding/disabling and
* showing/enabling of common elements when data is being transmitted or not.
*/
// Callback to disable some elements for the user when the client is busy.
function disableElementsGeneralCallback(resultsList, detail) {
if (detail.client.isBusy) {
resultsList.fullResultsCreate.classList.toggle('disabled', true);
resultsList.subResultsCreate.classList.toggle('disabled', true);
resultsList.toggleInspectButtons(detail.client);
}
}
// Callback to enable some elements for the user when the client is not busy.
function enableElementsGeneralCallback(resultsList, detail) {
if (!detail.client.isBusy) {
resultsList.fullResultsCreate.classList.toggle('disabled', false);
if (Object.keys(resultsList.subResultsIndexes).length > 0) {
resultsList.subResultsCreate.classList.toggle('disabled', false);
}
resultsList.toggleInspectButtons(detail.client);
}
}
/**
* Callback opening the loading modal when the client is connecting to the
* CQP server.
*/
function connectingCallback(resultsList, detail) {
resultsList.getHTMLElements(['#analysis-init-modal']);
resultsList.analysisInitModal = M.Modal.init(resultsList.analysisInitModal,
{dismissible: false});
resultsList.analysisInitModal.open();
}
// Callback that closes the loading modal from above.
function connectedCallback(resultsList, detail) {
/**
* In the past this closed the init modal. But the init modal is now being
* closed when the meta data has also been recieved. See below.
*/
}
// Callback that closes the loading modal from above.
function metaDataRecievedCallback(resultsList, detail) {
resultsList.analysisInitModal.close();
}
// Callback that shows the user some feedback if the client raised an error.
function clientFailedCallback(resultsList, detail) {
resultsList.getHTMLElements([
'#analysis-init-progress',
'#analysis-init-error',
'#user-feedback',
]);
if (detail.raiseModalFeedback) {
resultsList.analysisInitModal.open();
resultsList.analysisInitProgress.classList.toggle('hide');
resultsList.analysisInitError.classList.toggle('hide');
resultsList.analysisInitError.textContent = detail.msg;
} else {
nopaque.appClient.flash(detail.msg, 'error')
}
}
// Callback doing some preperation work if a query has been issued by the user.
function queryDataPreparingCallback(resultsList, detail) {
// remove all items from resultsList, like from the query issued before
resultsList.clear()
// get needed HTML Elements
let results = detail.results;
resultsList.getHTMLElements([
'#interactions-menu',
'#recieved-match-count',
'#total-match-count',
'#text-lookup-titles',
'#text-lookup-count',
'#query-results-user-feedback',
'#query-progress-bar',
'#query-results-create',
'#sub-results-indexes-display',
'#nr-marked-matches',
]);
// show or enable some things for the user
resultsList.interactionsMenu.classList.toggle('hide', false);
resultsList.queryResultsUserFeedback.classList.toggle('hide', false);
resultsList.queryProgressBar.classList.toggle('hide', false);
resultsList.showCorpusFiles.classList.toggle('disabled', true);
/**
* Set some initial values for the user feedback
* or reset values for new issued query
*/
resultsList.recievedMatchCount.textContent = 0;
resultsList.totalMatchCount.textContent = results.data.match_count;
resultsList.textLookupCount.textContent = 0;
resultsList.nrMarkedMatches.textContent = 0;
resultsList.subResultsIndexesDisplay.textContent = '';
resultsList.resetFields();
}
/**
* Callback handling the incoming results of an issued query. It renders
* the incoming matches using the resultsList for the user.
*/
function queryDataRecievingCallback(resultsList, detail) {
// load the data into the resultsList and show them to the user
let results = detail.results;
let client = detail.client;
let start = detail.dataLength;
let resultItems = [];
for (let [index, match] of Object.entries(results.data.matches).slice(start)) {
resultItems.push({ ...match, ...{ 'index': parseInt(index) } });
}
if (client.dynamicMode) {
resultsList.add(resultItems, (items) => {
for (let item of items) {
item.elm = resultsList.createResultRowElement(item,
results.data,
client);
}
});
// update user feedback about query status
resultsList.recievedMatchCount.textContent = results.data.matches.length;
resultsList.queryProgressBar.firstElementChild.style.width = `${client.requestQueryProgress}%`;
resultsList.textLookupCount.textContent = `${Object.keys(results.data.text_lookup).length}`;
// updating table on finished item creation callback via createResultRowElement
resultsList.update();
resultsList.changeHitsPerPage(client, results);
resultsList.changeContext();
//activate expertMode of switch is checked
if (resultsList.displayOptionsFormExpertMode.checked) {
resultsList.expertModeOn('query-display', results);
}
} else if (!client.dynamicMode) {
resultsList.add(resultItems, (items) => {
for (let item of items) {
item.elm = resultsList.createResultRowElement(item,
results.data,
client,
true);
}
});
// update user feedback about query status
resultsList.recievedMatchCount.textContent = results.data.matches.length;
resultsList.queryProgressBar.firstElementChild.style.width = `${client.requestQueryProgress}%`;
resultsList.textLookupCount.textContent = `${Object.keys(results.data.text_lookup).length}`;
// updating table on finished item creation callback via createResultRowElement
resultsList.update();
resultsList.changeHitsPerPage(client, results);
resultsList.changeContext();
}
}
// Callback that is executed when all results from an issued query have been recieved
function queryDataRecievedCallback(resultsList, detail) {
// hide or disable some things for the user
resultsList.queryResultsUserFeedback.classList.toggle('hide', true);
resultsList.queryProgressBar.classList.toggle('hide', true);
// reset bar progress for next query
resultsList.queryProgressBar.firstElementChild.style.width = '0%';
resultsList.showCorpusFiles.classList.toggle('disabled', false);
}
/**
* Callback that is handling incoming results data. Results data is needed for
* the export and download of the data.
*/
function resultsDataRecievingCallback(resultsList, detail) {
resultsList.getHTMLElements([
'#full-results-progress-bar',
'#sub-results-progress-bar',
]);
// Disable the full context switch when results are being recieved.
resultsList.exportFullInspectContext.setAttribute('disabled', '');
if (detail.type === 'full-results' && detail.progress) {
resultsList.fullResultsProgressBar.firstElementChild.style.width = `${detail.progress}%`;
resultsList.fullResultsProgressBar.classList.toggle('hide', false);
} else if (detail.type === 'sub-results' && detail.progress) {
resultsList.subResultsProgressBar.firstElementChild.style.width = `${detail.progress}%`;
resultsList.subResultsProgressBar.classList.toggle('hide', false);
}
}
/**
* Callback is executed when all results data has been recieved.
* Reactivates the resutls create buttons etc.
*/
function resultsDataRecievedCallback(resultsList, detail) {
// create strings for create buttons depending on type
const handleType = (keyPrefix, text) => {
// Enable the full context switch when results have been recieved
resultsList.exportFullInspectContext.removeAttribute('disabled', '');
// hides the create element after results have been recieved and reset it
resultsList[`${keyPrefix}Create`].classList.toggle('hide');
resultsList[`${keyPrefix}Create`].textContent = `Create ${text}`;
resultsList[`${keyPrefix}Create`].insertAdjacentHTML('beforeend',
`<i class="material-icons left">build</i>`);
// show and highlight export button
resultsList[`${keyPrefix}Export`].classList.toggle('hide', false);
resultsList[`${keyPrefix}Export`].classList.toggle('pulse', true);
setTimeout(() => {
resultsList[`${keyPrefix}Export`].classList.toggle('pulse', false);
clearTimeout();
}, 3000)
}
if (detail.type === 'full-results') {
handleType('fullResults', 'Results');
if (detail.fullContext) {
resultsList.fullResultsProgressBar.firstElementChild.style.width = `0%`;
resultsList.fullResultsProgressBar.classList.toggle('hide', true);
}
} else if (detail.type ==='sub-results') {
handleType('subResults', 'Sub-Results');
if (detail.fullContext) {
resultsList.subResultsProgressBar.firstElementChild.style.width = `0%`;
resultsList.subResultsProgressBar.classList.toggle('hide', true);
}
} else if (detail.type ==='inspect-results') {
if (Object.keys(resultsList.subResultsIndexes).length === 0) {
/**
* Prevent create sub results button from being activated if it is disabled
* and no matches have been marked by the user for sub results creation.
*/
resultsList.subResultsCreate.classList.toggle('disabled', true);
}
resultsList.showMatchContext(detail.results, detail.client);
}
}
// export the callbacks
export {
connectingCallback,
connectedCallback,
metaDataRecievedCallback,
clientFailedCallback,
queryDataPreparingCallback,
queryDataRecievingCallback,
queryDataRecievedCallback,
resultsDataRecievingCallback,
resultsDataRecievedCallback,
disableElementsGeneralCallback,
enableElementsGeneralCallback,
};
@@ -0,0 +1,378 @@
/**
* 1.)
* This file contains the listener function that will be assigned to the
* corpus_analysis ResultsView. The listener is listening for the notification
* event which is being dispatched by the corpus_analysis Client. The
* notification Event triggers the listener which will call different
* callback functions depending on the detail information of the notification
* event.
* 2.)
* This file also contains vanilla javascript Event listeners which are
* listening for button clicks etc. the user is doing for page interaction.
* They will be registered the same way as teh listeners above.
*/
import {
connectingCallback,
connectedCallback,
metaDataRecievedCallback,
clientFailedCallback,
queryDataPreparingCallback,
queryDataRecievingCallback,
queryDataRecievedCallback,
resultsDataRecievingCallback,
resultsDataRecievedCallback,
disableElementsGeneralCallback,
enableElementsGeneralCallback,
} from './callbacks.js';
// Import the script that implements a spinner animation for buttons.
import {
loadingSpinnerHTML,
} from './spinner.js';
/**
* The Listener listening for the notification event 'notify-view' dispatched
* by the client and execeutes callbacks accordingly.
*/
function recieveClientNotification(eventType, resultsList) {
document.addEventListener(eventType, (event) => {
let caseIdentifier = event.detail.caseIdentifier;
switch (caseIdentifier) {
case 'client-failed':
console.error('View recieved notification:', caseIdentifier);
// execute callbacks
clientFailedCallback(resultsList, event.detail);
break;
case 'connecting':
console.info('View recieved notification:', caseIdentifier);
connectingCallback(resultsList, event.detail);
// execute callbacks
break;
case 'connected':
console.info('View recieved notification:', caseIdentifier);
connectedCallback(resultsList, event.detail);
break;
case 'meta-data-recieving':
console.info('View recieved notification:', caseIdentifier);
break;
case 'meta-data-recieved':
console.info('View recieved notification:', caseIdentifier);
// execute
metaDataRecievedCallback(resultsList, event.detail);
break;
case 'query-data-prepareing':
console.info('View recieved notification:', caseIdentifier);
// Hide all download buttons
resultsList.fullResultsExport.classList.toggle('hide', true);
resultsList.subResultsExport.classList.toggle('hide', true);
// Show all create buttons
resultsList.fullResultsCreate.classList.toggle('hide', false);
resultsList.subResultsCreate.classList.toggle('hide', false);
// execute callbacks
disableElementsGeneralCallback(resultsList, event.detail);
queryDataPreparingCallback(resultsList, event.detail);
break;
case 'query-data-recieving':
console.info('View recieved notification:', caseIdentifier);
// execute callbacks
queryDataRecievingCallback(resultsList, event.detail);
break;
case 'query-data-recieved':
console.info('View recieved notification:', caseIdentifier);
// execute callbacks
queryDataRecievedCallback(resultsList, event.detail);
enableElementsGeneralCallback(resultsList, event.detail);
// create sub-results is disabled per default until matches have been added
resultsList.subResultsCreate.classList.toggle('disabled', true);
break;
case 'results-data-recieving':
console.info('View recieved notification:', caseIdentifier);
// execute callbacks
disableElementsGeneralCallback(resultsList, event.detail);
resultsDataRecievingCallback(resultsList, event.detail);
break;
case 'results-data-recieved':
console.info('View recieved notification:', caseIdentifier);
// execute callbacks
console.info(event.detail);
resultsDataRecievedCallback(resultsList, event.detail);
enableElementsGeneralCallback(resultsList, event.detail);
break;
default:
console.error('Recieved unkown notification case identifier from Client');
// do something to not crash the analysis session?
}
});
}
/**
* This are some vanilla javascript Event listeners which are listening
* for button clicks etc. the user is doing to interact with the page.
* They will be registered the same way as the listeners above.
*/
/**
* The following listener handles what functions are called when the user
* does use the page navigation to navigate to a new page.
*/
function pageNavigation(resultsList, results, client) {
for (let element of resultsList.pagination) {
element.addEventListener("click", (event) => {
// Shows match context according to the user picked value on a new page.
resultsList.changeContext();
// De- or activates expertMode on new page depending on switch value.
if (resultsList.displayOptionsFormExpertMode.checked) {
resultsList.expertModeOn('query-display', results);
} else {
resultsList.expertModeOff('query-display');
}
// Activates inspect buttons on new page if client is not busy.
resultsList.toggleInspectButtons(client);
});
}
}
/**
* The following event Listener handles the expert mode switch for the list.
*/
function expertModeSwitch(resultsList, results) {
resultsList.displayOptionsFormExpertMode.onchange = (event) => {
if (event.target.checked) {
resultsList.expertModeOn('query-display', results);
} else {
resultsList.expertModeOff('query-display');
}
};
}
/**
* The following event Listener handles the add-btn and the inspect-btn
* onclick events via bubbleing.
*/
function actionButtons(resultsList, results, client) {
resultsList.queryResultsTable.addEventListener('click', (event) => {
let dataIndex;
if (event.target.classList.contains('inspect-btn')) {
dataIndex = parseInt(event.target.closest('tr').dataset.index);
resultsList.inspect(client, results, [dataIndex], 'inspect');
} else if (event.target.classList.contains('add-btn')) {
dataIndex = parseInt(event.target.closest('tr').dataset.index);
resultsList.addToSubResults(dataIndex, client);
}
})
}
/**
* Following event listeners handle the change of Context size per match and
* the number of matches shown per page.
*/
function displayOptions(resultsList, results, client) {
resultsList.displayOptionsFormResultsPerPage.onchange = (event) => {
resultsList.changeHitsPerPage(client, results);
};
resultsList.displayOptionsFormResultContext.onchange = (event) => {
resultsList.changeContext();
};
}
/**
* The following event listener handles the show metadata button and its
* functionality.
*/
function showMetaData(resultsList, results) {
resultsList.showMetaData.onclick = () => {
resultsList.metaDataModalContent.textContent = '';
let table = resultsList.createMetaDataForModal(results.metaData);
resultsList.metaDataModalContent.insertAdjacentHTML('afterbegin', table);
resultsList.metaDataModal.open();
let collapsibles = resultsList.metaDataModalContent.querySelectorAll(".text-metadata");
for (let collapsible of collapsibles) {
collapsible.onclick = () => {
let elems = resultsList.metaDataModalContent.querySelectorAll('.collapsible');
let instances = M.Collapsible.init(elems, {accordion: false});
resultsList.createTextDetails(results.metaData);
}
}
};
}
/**
* The following event listener handles the button showing infos about matches
* and their corresponding corpus files
*/
function showCorpusFiles(resultsList, results) {
resultsList.showCorpusFiles.onclick = () => {
resultsList.showCorpusFilesModalContent.innerHTML = '';
let htmlString = `
<div id="corpus-file-table">
<ul class="pagination paginationTop"></ul>
<table class="responsive-table highlight">
<thead>
<tr>
<th class="sort" data-sort="title">Title</th>
<th class="sort" data-sort="year">Year</th>
<th class="sort" data-sort="match-count">Match count in this text</th>
</tr>
</thead>
<tbody class="list">
`
for (let [key, value] of Object.entries(results.data.text_lookup)) {
htmlString += `
<tr>
<td class="title">${value.title}</td>
<td class="year">${value.publishing_year}</td>
<td class="match-count">${value.match_count}</td>
</tr>
`
}
htmlString += `
</tbody>
</table>
<ul class="pagination paginationBottom"></ul>
</div>
`
resultsList.showCorpusFilesModalContent.insertAdjacentHTML('afterbegin', htmlString);
resultsList.showCorpusFilesModal.open();
let options = {
page: 10,
pagination: [{
name: "paginationTop",
paginationClass: "paginationTop",
innerWindow: 8,
outerWindow: 1
}, {
paginationClass: "paginationBottom",
innerWindow: 8,
outerWindow: 1
}],
valueNames: ["title", "year", "match-count"],
};
let corpusFileTable = new List('corpus-file-table', options);
}
}
/**
* Checks if resultsList.exportFullInspectContext switch is changed.
* If it has been changed reset all Download buttons.
*/
function exportFullContextSwitch(resultsList) {
resultsList.exportFullInspectContext.onchange = (event) => {
// Hide all download buttons.
resultsList.fullResultsExport.classList.toggle('hide', true);
resultsList.subResultsExport.classList.toggle('hide', true);
// Show result create buttons.
resultsList.fullResultsCreate.classList.toggle('hide', false);
resultsList.subResultsCreate.classList.toggle('hide', false);
}
}
/**
* The following event listeners are handeling the data export.
* 1. Create full-results
* 2. Create sub-results
* 3. Download full-results
* 4. Download sub-results
* 5. Download single inspect-results
*/
// 1. Add events for full-results create
function createFullResults(resultsList, results) {
resultsList.fullResultsCreate.onclick = (event) => {
resultsList.fullResultsCreate.querySelector('i').classList.toggle('hide');
resultsList.fullResultsCreate.textContent = 'Creating...';
resultsList.fullResultsCreate.insertAdjacentHTML('afterbegin',
loadingSpinnerHTML);
// .keys() is for a zero based array. I think...
let dataIndexes = [...Array(results.data.match_count).keys()];
// Empty fullResultsData so that no previous data is used.
results.fullResultsData.init();
resultsList.notifyClient('get-results', {resultsType: 'full-results',
dataIndexes: dataIndexes,
resultsList: resultsList,});
}
}
// 2. Add events for sub-results create
function createSubResults(resultsList, results) {
resultsList.subResultsCreate.onclick = (event) => {
let dataIndexes = [];
Object.keys(resultsList.subResultsIndexes).forEach((id) => {
dataIndexes.push(id);
});
resultsList.subResultsCreate.querySelector('i').classList.toggle('hide');
resultsList.subResultsCreate.textContent = 'Creating...';
resultsList.subResultsCreate.insertAdjacentHTML('afterbegin',
loadingSpinnerHTML);
// Empty subResultsData so that no previous data is used.
results.subResultsData.init();
resultsList.notifyClient('get-results', {resultsType: 'sub-results',
dataIndexes: dataIndexes,
resultsList: resultsList,});
}
}
// 3. Open download modal when full results export button is pressed
function exportFullResults(resultsList, results) {
resultsList.fullResultsExport.onclick = (event) => {
resultsList.queryResultsDownloadModal.open();
// add onclick to download JSON button and download the file
resultsList.downloadResultsJson.onclick = (event) => {
let suffix = 'full-results'
if (resultsList.exportFullInspectContext.checked) {
suffix += '_full-context';
}
let filename = results.fullResultsData.createDownloadFilename(suffix);
results.fullResultsData.addData(results.metaData);
results.fullResultsData.downloadJSONRessource(filename,
results.fullResultsData,
resultsList.downloadResultsJson)};
}
}
// 4. Open download modal when sub results export button is pressed
function exportSubResults(resultsList, results) {
resultsList.subResultsExport.onclick = (event) => {
resultsList.queryResultsDownloadModal.open();
// add onclick to download JSON button and download the file
resultsList.downloadResultsJson.onclick = (event) => {
let suffix = 'sub-results'
if (resultsList.exportFullInspectContext.checked) {
suffix += '_full-context';
}
let filename = results.subResultsData.createDownloadFilename(suffix);
results.subResultsData.addData(results.metaData);
results.subResultsData.downloadJSONRessource(filename,
results.subResultsData,
resultsList.downloadResultsJson)};
}
}
// 5. Open download modal when inspect-results-export button is pressed
function exportSingleMatch(resultsList, results) {
resultsList.inspectResultsExport.onclick = (event) => {
resultsList.queryResultsDownloadModal.open();
// add onclick to download JSON button and download the file
resultsList.downloadResultsJson.onclick = (event) => {
let filename = results.subResultsData.createDownloadFilename('inspect-results_full-context');
results.subResultsData.addData(results.metaData);
results.subResultsData.downloadJSONRessource(filename,
results.inspectResultsData,
resultsList.downloadResultsJson)};
}
}
// export listeners
export {
recieveClientNotification,
pageNavigation,
expertModeSwitch,
actionButtons,
displayOptions,
showMetaData,
showCorpusFiles,
exportFullContextSwitch,
createFullResults,
createSubResults,
exportFullResults,
exportSubResults,
exportSingleMatch,
};
@@ -0,0 +1,27 @@
/**
* Function to show a scroll to top button if the user has scrolled down
* 250 pixels from the with scrollToElementSelector specified Element.
*/
function scrollToTop(scrollToElementSelector, triggerElementSelector) {
let scrollToThis = document.querySelector(scrollToElementSelector);
let scrolltoTopTrigger = document.querySelector(triggerElementSelector);
window.addEventListener('scroll', (event) => {
if (pageYOffset > 250) {
scrolltoTopTrigger.classList.toggle('hide', false);
} else {
scrolltoTopTrigger.classList.toggle('hide', true);
}
});
scrolltoTopTrigger.onclick = () => {
scrollToThis.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'nearest'
});
};
}
// Export function.
export {
scrollToTop
};
@@ -0,0 +1,19 @@
// loading spinner animation HTML
const loadingSpinnerHTML = `
<div class="preloader-wrapper button-icon-spinner small active">
<div class="spinner-layer spinner-green-only">
<div class="circle-clipper left">
<div class="circle"></div>
</div><div class="gap-patch">
<div class="circle"></div>
</div><div class="circle-clipper right">
<div class="circle"></div>
</div>
</div>
</div>
`;
// Export const.
export {
loadingSpinnerHTML
};
@@ -0,0 +1,103 @@
class CorpusDisplay extends RessourceDisplay {
constructor(displayElement) {
super(displayElement);
this.corpusId = displayElement.dataset.corpusId;
this.user.eventListeners.corpus.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), this.corpusId);
}
init() {
for (let exportCorpusTriggerElement of this.displayElement.querySelectorAll('.export-corpus-trigger')) {exportCorpusTriggerElement.addEventListener('click', () => this.requestCorpusExport());}
nopaque.appClient.socket.on(`export_corpus_${this.user.data.corpora[this.corpusId].id}`, () => this.downloadCorpus());
this.setCreationDate(this.user.data.corpora[this.corpusId].creation_date);
this.setDescription(this.user.data.corpora[this.corpusId].description);
this.setLastEditedDate(this.user.data.corpora[this.corpusId].last_edited_date);
this.setStatus(this.user.data.corpora[this.corpusId].status);
this.setTitle(this.user.data.corpora[this.corpusId].title);
this.setTokenRatio(this.user.data.corpora[this.corpusId].current_nr_of_tokens, this.user.data.corpora[this.corpusId].max_nr_of_tokens);
}
patch(patch) {
let re;
for (let operation of patch) {
switch(operation.op) {
case 'replace':
// Matches: /jobs/{this.job.id}/status
re = new RegExp('^/corpora/' + this.user.data.corpora[this.corpusId].id + '/last_edited_date');
if (re.test(operation.path)) {this.setLastEditedDate(operation.value); break;}
// Matches: /jobs/{this.job.id}/status
re = new RegExp('^/corpora/' + this.user.data.corpora[this.corpusId].id + '/status$');
if (re.test(operation.path)) {this.setStatus(operation.value); break;}
break;
default:
break;
}
}
}
requestCorpusExport() {
nopaque.appClient.socket.emit('export_corpus', this.user.data.corpora[this.corpusId].id);
nopaque.appClient.flash('Preparing your corpus export...', 'corpus');
for (let exportCorpusTriggerElement of this.displayElement.querySelectorAll('.export-corpus-trigger')) {exportCorpusTriggerElement.classList.toggle('disabled', true);}
}
downloadCorpus() {
nopaque.appClient.flash('Corpus export is done. Your corpus download is ready!', 'corpus');
for (let exportCorpusTriggerElement of this.displayElement.querySelectorAll('.export-corpus-trigger')) {exportCorpusTriggerElement.classList.toggle('disabled', false);}
// Little trick to call the download view after ziping has finished
let fakeBtn = document.createElement('a');
fakeBtn.href = `/corpora/${this.user.data.corpora[this.corpusId].id}/download`;
fakeBtn.click();
}
setTitle(title) {
for (let element of this.displayElement.querySelectorAll('.corpus-title')) {this.setElement(element, title);}
}
setTokenRatio(currentNrOfTokens, maxNrOfTokens) {
let tokenRatio = `${currentNrOfTokens}/${maxNrOfTokens}`;
for (let element of this.displayElement.querySelectorAll('.corpus-token-ratio')) {this.setElement(element, tokenRatio);}
}
setDescription(description) {
for (let element of this.displayElement.querySelectorAll('.corpus-description')) {this.setElement(element, description);}
}
setStatus(status) {
for (let element of this.displayElement.querySelectorAll('.analyse-corpus-trigger')) {
if (['analysing', 'prepared', 'start analysis'].includes(status)) {
element.classList.remove('disabled');
} else {
element.classList.add('disabled');
}
}
for (let element of this.displayElement.querySelectorAll('.build-corpus-trigger')) {
if (status === 'unprepared' && Object.values(this.user.data.corpora[this.corpusId].files).length > 0) {
element.classList.remove('disabled');
} else {
element.classList.add('disabled');
}
}
for (let element of this.displayElement.querySelectorAll('.corpus-status')) {this.setElement(element, status);}
for (let exportCorpusTriggerElement of this.displayElement.querySelectorAll('.export-corpus-trigger')) {
exportCorpusTriggerElement.classList.toggle('disabled', !['prepared', 'start analysis', 'stop analysis'].includes(status));
}
for (let element of this.displayElement.querySelectorAll('.status')) {element.dataset.status = status;}
for (let element of this.displayElement.querySelectorAll('.status-spinner')) {
if (['submitted', 'queued', 'running', 'canceling', 'start analysis', 'stop analysis'].includes(status)) {
element.classList.remove('hide');
} else {
element.classList.add('hide');
}
}
}
setCreationDate(creationDateTimestamp) {
let creationDate = new Date(creationDateTimestamp * 1000).toLocaleString("en-US");
for (let element of this.displayElement.querySelectorAll('.corpus-creation-date')) {this.setElement(element, creationDate);}
}
setLastEditedDate(endDateTimestamp) {
let endDate = new Date(endDateTimestamp * 1000).toLocaleString("en-US");
for (let element of this.displayElement.querySelectorAll('.corpus-end-date')) {this.setElement(element, endDate);}
}
}
@@ -0,0 +1,87 @@
class JobDisplay extends RessourceDisplay {
constructor(displayElement) {
super(displayElement);
this.jobId = displayElement.dataset.jobId;
this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), this.jobId);
}
init(job) {
this.setCreationDate(this.user.data.jobs[this.jobId].creation_date);
this.setEndDate(this.user.data.jobs[this.jobId].creation_date);
this.setDescription(this.user.data.jobs[this.jobId].description);
this.setService(this.user.data.jobs[this.jobId].service);
this.setServiceArgs(this.user.data.jobs[this.jobId].service_args);
this.setServiceVersion(this.user.data.jobs[this.jobId].service_version);
this.setStatus(this.user.data.jobs[this.jobId].status);
this.setTitle(this.user.data.jobs[this.jobId].title);
}
patch(patch) {
let re;
for (let operation of patch) {
switch(operation.op) {
case 'replace':
// Matches: /jobs/{this.user.data.jobs[this.jobId].id}/status
re = new RegExp('^/jobs/' + this.user.data.jobs[this.jobId].id + '/end_date');
if (re.test(operation.path)) {this.setEndDate(operation.value); break;}
// Matches: /jobs/{this.user.data.jobs[this.jobId].id}/status
re = new RegExp('^/jobs/' + this.user.data.jobs[this.jobId].id + '/status$');
if (re.test(operation.path)) {this.setStatus(operation.value); break;}
break;
default:
break;
}
}
}
setTitle(title) {
for (let element of this.displayElement.querySelectorAll('.job-title')) {this.setElement(element, title);}
}
setDescription(description) {
for (let element of this.displayElement.querySelectorAll('.job-description')) {this.setElement(element, description);}
}
setStatus(status) {
for (let element of this.displayElement.querySelectorAll('.job-status')) {
this.setElement(element, status);
}
for (let element of this.displayElement.querySelectorAll('.status')) {element.dataset.status = status;}
for (let element of this.displayElement.querySelectorAll('.status-spinner')) {
if (['complete', 'failed'].includes(status)) {
element.classList.add('hide');
} else {
element.classList.remove('hide');
}
}
for (let element of this.displayElement.querySelectorAll('.restart-job-trigger')) {
if (['complete', 'failed'].includes(status)) {
element.classList.remove('hide');
} else {
element.classList.add('hide');
}
}
}
setCreationDate(creationDateTimestamp) {
let creationDate = new Date(creationDateTimestamp * 1000).toLocaleString("en-US");
for (let element of this.displayElement.querySelectorAll('.job-creation-date')) {this.setElement(element, creationDate);}
}
setEndDate(endDateTimestamp) {
let endDate = new Date(endDateTimestamp * 1000).toLocaleString("en-US");
for (let element of this.displayElement.querySelectorAll('.job-end-date')) {this.setElement(element, endDate);}
}
setService(service) {
for (let element of this.displayElement.querySelectorAll('.job-service')) {this.setElement(element, service);}
}
setServiceArgs(serviceArgs) {
for (let element of this.displayElement.querySelectorAll('.job-service-args')) {this.setElement(element, serviceArgs);}
}
setServiceVersion(serviceVersion) {
for (let element of this.displayElement.querySelectorAll('.job-service-version')) {this.setElement(element, serviceVersion);}
}
}
@@ -0,0 +1,45 @@
class RessourceDisplay {
constructor(displayElement) {
if (displayElement.dataset.userId) {
if (displayElement.dataset.userId in nopaque.appClient.users) {
this.user = nopaque.appClient.users[displayElement.dataset.userId];
} else {
console.error(`User not found: ${displayElement.dataset.userId}`);
return;
}
} else {
this.user = nopaque.appClient.users.self;
}
this.displayElement = displayElement;
}
eventHandler(eventType, payload) {
switch (eventType) {
case 'init':
this.init(payload);
break;
case 'patch':
this.patch(payload);
break;
default:
console.error(`Unknown event type: ${eventType}`);
break;
}
}
init() {console.error('init method not implemented!');}
patch() {console.error('patch method not implemented!');}
setElement(element, value) {
switch (element.tagName) {
case 'INPUT':
element.value = value;
M.updateTextFields();
break;
default:
element.innerText = value;
break;
}
}
}
@@ -0,0 +1,97 @@
class CorpusFileList extends RessourceList {
constructor(listElement, options = {}) {
super(listElement, {...CorpusFileList.options, ...options});
this.corpusId = listElement.dataset.corpusId;
this.user.eventListeners.corpus.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), this.corpusId);
}
init() {
super.init(this.user.data.corpora[this.corpusId].files);
}
onclick(event) {
let ressourceElement = event.target.closest('tr');
if (ressourceElement === null) {return;}
let corpusFileId = ressourceElement.dataset.id;
let actionButtonElement = event.target.closest('.action-button');
if (actionButtonElement === null) {return;}
let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
switch (action) {
case 'delete':
let deleteModalHTML = `<div class="modal">
<div class="modal-content">
<h4>Confirm corpus deletion</h4>
<p>Do you really want to delete the corpus file <b>${this.user.data.corpora[this.corpusId].files[corpusFileId].filename}</b>? It will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light" href="${this.user.data.corpora[this.corpusId].files[corpusFileId].url}/delete"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>`;
let deleteModalParentElement = document.querySelector('#modals');
deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML);
let deleteModalElement = deleteModalParentElement.lastChild;
let deleteModal = M.Modal.init(deleteModalElement, {onCloseEnd: () => {deleteModal.destroy(); deleteModalElement.remove();}});
deleteModal.open();
break;
case 'download':
window.location.href = this.user.data.corpora[this.corpusId].files[corpusFileId].download_url;
break;
case 'view':
if (corpusFileId !== '-1') {window.location.href = this.user.data.corpora[this.corpusId].files[corpusFileId].url;}
break;
default:
console.error(`Unknown action: "${action}"`);
break;
}
}
patch(patch) {
let id, match, re, valueName;
for (let operation of patch) {
switch(operation.op) {
case 'add':
// Matches the only paths that should be handled here: /corpora/{this.user.data.corpora[this.corpusId].id}/files/{corpusFileId}
re = new RegExp('^/corpora/' + this.user.data.corpora[this.corpusId].id + '/files/(\\d+)$');
if (re.test(operation.path)) {this.add(operation.value);}
break;
case 'remove':
// See case add ;)
re = new RegExp('^/corpora/' + this.user.data.corpora[this.corpusId].id + '/files/(\\d+)$');
if (re.test(operation.path)) {
[match, id] = operation.path.match(re);
this.remove(id);
}
break;
case 'replace':
// Matches the only paths that should be handled here: /corpora/{corpusId}/{status || description || title}
re = new RegExp('^/corpora/' + this.user.data.corpora[this.corpusId].id + '/files/(\\d+)/(author|filename|publishing_year|title)$');
if (re.test(operation.path)) {
[match, id, valueName] = operation.path.match(re);
this.replace(id, valueName, operation.value);
}
break;
default:
break;
}
}
}
preprocessRessource(corpusFile) {
return {id: corpusFile.id, author: corpusFile.author, filename: corpusFile.filename, publishing_year: corpusFile.publishing_year, title: corpusFile.title};
}
}
CorpusFileList.options = {
item: `<tr>
<td><span class="filename"></span></td>
<td><span class="author"></span></td>
<td><span class="title"></span></td>
<td><span class="publishing_year"></span></td>
<td class="right-align">
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="download" data-position="top" data-tooltip="View"><i class="material-icons">file_download</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
</td>
</tr>`,
valueNames: [{data: ['id']}, 'author', 'filename', 'publishing_year', 'title']
};
@@ -0,0 +1,93 @@
class CorpusList extends RessourceList {
constructor(listElement, options = {}) {
super(listElement, {...CorpusList.options, ...options});
this.user.eventListeners.corpus.addEventListener((eventType, payload) => this.eventHandler(eventType, payload));
}
init() {
super.init(this.user.data.corpora);
}
onclick(event) {
let ressourceElement = event.target.closest('tr');
if (ressourceElement === null) {return;}
let corpusId = ressourceElement.dataset.id;
let actionButtonElement = event.target.closest('.action-button');
let action = (actionButtonElement === null) ? 'view' : actionButtonElement.dataset.action;
switch (action) {
case 'delete':
let deleteModalHTML = `<div class="modal">
<div class="modal-content">
<h4>Confirm corpus deletion</h4>
<p>Do you really want to delete the corpus <b>${this.user.data.corpora[corpusId].title}</b>? All files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light" href="${this.user.data.corpora[corpusId].url}/delete"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>`;
let deleteModalParentElement = document.querySelector('#modals');
deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML);
let deleteModalElement = deleteModalParentElement.lastChild;
let deleteModal = M.Modal.init(deleteModalElement, {onCloseEnd: () => {deleteModal.destroy(); deleteModalElement.remove();}});
deleteModal.open();
break;
case 'view':
if (corpusId !== '-1') {window.location.href = this.user.data.corpora[corpusId].url;}
break;
default:
console.error(`Unknown action: ${action}`);
break;
}
}
patch(patch) {
let id, match, re, valueName;
for (let operation of patch) {
switch(operation.op) {
case 'add':
// Matches the only paths that should be handled here: /corpora/{corpusId}
re = /^\/corpora\/(\d+)$/;
if (re.test(operation.path)) {this.add(operation.value);}
break;
case 'remove':
// See case 'add' ;)
re = /^\/corpora\/(\d+)$/;
if (re.test(operation.path)) {
[match, id] = operation.path.match(re);
this.remove(id);
}
break;
case 'replace':
// Matches the only paths that should be handled here: /corpora/{corpusId}/{status || description || title}
re = /^\/corpora\/(\d+)\/(status|description|title)$/;
if (re.test(operation.path)) {
[match, id, valueName] = operation.path.match(re);
this.replace(id, valueName, operation.value);
}
break;
default:
break;
}
}
}
preprocessRessource(corpus) {
return {id: corpus.id,
status: corpus.status,
description: corpus.description,
title: corpus.title};
}
}
CorpusList.options = {
item: `<tr>
<td><a class="btn-floating disabled"><i class="material-icons service-color darken" data-service="corpus-analysis">book</i></a></td>
<td><b class="title"></b><br><i class="description"></i></td>
<td><span class="badge new status status-color status-text" data-badge-caption=""></span></td>
<td class="right-align">
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
</td>
</tr>`,
valueNames: [{data: ['id']}, {name: 'status', attr: 'data-status'}, 'description', 'title']
};
@@ -0,0 +1,41 @@
class JobInputList extends RessourceList {
constructor(listElement, options = {}) {
super(listElement, {...JobInputList.options, ...options});
this.jobId = listElement.dataset.jobId;
this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), this.jobId);
}
init() {
super.init(this.user.data.jobs[this.jobId].inputs);
}
onclick(event) {
let ressourceElement = event.target.closest('tr');
if (ressourceElement === null) {return;}
let jobInputId = ressourceElement.dataset.id;
let actionButtonElement = event.target.closest('.action-button');
if (actionButtonElement === null) {return;}
let action = actionButtonElement.dataset.action;
switch (action) {
case 'download':
window.location.href = this.user.data.jobs[this.jobId].inputs[jobInputId].download_url;
break;
default:
console.error(`Unknown action: "${action}"`);
break;
}
}
preprocessRessource(jobInput) {
return {id: jobInput.id, filename: jobInput.filename};
}
}
JobInputList.options = {
item: `<tr>
<td><span class="filename"></span></td>
<td class="right-align">
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="download" data-position="top" data-tooltip="View"><i class="material-icons">file_download</i></a>
</td>
</tr>`,
valueNames: [{data: ['id']}, 'filename']
};
@@ -0,0 +1,94 @@
class JobList extends RessourceList {
constructor(listElement, options = {}) {
super(listElement, {...JobList.options, ...options});
this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload));
}
init() {
super.init(this.user.data.jobs);
}
onclick(event) {
let ressourceElement = event.target.closest('tr');
if (ressourceElement === null) {return;}
let jobId = ressourceElement.dataset.id;
let actionButtonElement = event.target.closest('.action-button');
let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
switch (action) {
case 'delete':
let deleteModalHTML = `<div class="modal">
<div class="modal-content">
<h4>Confirm job deletion</h4>
<p>Do you really want to delete the job <b>${this.user.data.jobs[jobId].title}</b>? All files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light" href="${this.user.data.jobs[jobId].url}/delete"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>`;
let deleteModalParentElement = document.querySelector('#modals');
deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML);
let deleteModalElement = deleteModalParentElement.lastChild;
let deleteModal = M.Modal.init(deleteModalElement, {onCloseEnd: () => {deleteModal.destroy(); deleteModalElement.remove();}});
deleteModal.open();
break;
case 'view':
if (jobId !== '-1') {window.location.href = this.user.data.jobs[jobId].url;}
break;
default:
console.error(`Unknown action: "${action}"`);
break;
}
}
patch(patch) {
let id, match, re, valueName;
for (let operation of patch) {
switch(operation.op) {
case 'add':
// Matches the only paths that should be handled here: /jobs/{jobId}
re = /^\/jobs\/(\d+)$/;
if (re.test(operation.path)) {this.add(operation.value);}
break;
case 'remove':
// See case add ;)
re = /^\/jobs\/(\d+)$/;
if (re.test(operation.path)) {
[match, id] = operation.path.match(re);
this.remove(id);
}
break;
case 'replace':
// Matches the only paths that should be handled here: /jobs/{jobId}/{service || status || description || title}
re = /^\/jobs\/(\d+)\/(service|status|description|title)$/;
if (re.test(operation.path)) {
[match, id, valueName] = operation.path.match(re);
this.replace(id, valueName, operation.value);
}
break;
default:
break;
}
}
}
preprocessRessource(job) {
return {id: job.id,
service: job.service,
status: job.status,
description: job.description,
title: job.title};
}
}
JobList.options = {
item: `<tr>
<td><a class="btn-floating disabled"><i class="nopaque-icons service service-color darken service-icon"></i></a></td>
<td><b class="title"></b><br><i class="description"></i></td>
<td><span class="badge new status status-color status-text" data-badge-caption=""></span></td>
<td class="right-align">
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
</td>
</tr>`,
valueNames: [{data: ['id']}, {name: 'service', attr: 'data-service'}, {name: 'status', attr: 'data-status'}, 'description', 'title']
};
@@ -0,0 +1,71 @@
class JobResultList extends RessourceList {
constructor(listElement, options = {}) {
super(listElement, {...JobResultList.options, ...options});
this.jobId = listElement.dataset.jobId;
this.user.eventListeners.job.addEventListener((eventType, payload) => this.eventHandler(eventType, payload), this.jobId);
}
init() {
super.init(this.user.data.jobs[this.jobId].results);
}
onclick(event) {
let ressourceElement = event.target.closest('tr');
if (ressourceElement === null) {return;}
let jobResultId = ressourceElement.dataset.id;
let actionButtonElement = event.target.closest('.action-button');
if (actionButtonElement === null) {return;}
let action = actionButtonElement.dataset.action;
switch (action) {
case 'download':
window.location.href = this.user.data.jobs[this.jobId].results[jobResultId].download_url;
break;
default:
console.error(`Unknown action: "${action}"`);
break;
}
}
patch(patch) {
let re;
for (let operation of patch) {
switch(operation.op) {
case 'add':
// Matches the only paths that should be handled here: /jobs/{this.user.data.jobs[this.jobId].id}/results/{jobResultId}
re = new RegExp('^/jobs/' + this.user.data.jobs[this.jobId].id + '/results/(\\d+)$');
if (re.test(operation.path)) {this.add(operation.value);}
break;
default:
break;
}
}
}
preprocessRessource(jobResult) {
let description;
if (jobResult.filename.endsWith('.pdf.zip')) {
description = 'PDF files with text layer';
} else if (jobResult.filename.endsWith('.txt.zip')) {
description = 'Raw text files';
} else if (jobResult.filename.endsWith('.vrt.zip')) {
description = 'VRT compliant files including the NLP data';
} else if (jobResult.filename.endsWith('.xml.zip')) {
description = 'TEI compliant files';
} else if (jobResult.filename.endsWith('.poco.zip')) {
description = 'HOCR and image files for post correction (PoCo)';
} else {
description = 'All result files created during this job';
}
return {id: jobResult.id, description: description, filename: jobResult.filename};
}
}
JobResultList.options = {
item: `<tr>
<td><span class="description"></span></td>
<td><span class="filename"></span></td>
<td class="right-align">
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="download" data-position="top" data-tooltip="View"><i class="material-icons">file_download</i></a>
</td>
</tr>`,
valueNames: [{data: ['id']}, 'description', 'filename']
};
@@ -0,0 +1,93 @@
class QueryResultList extends RessourceList {
constructor(listElement, options = {}) {
super(listElement, {...QueryResultList.options, ...options});
this.user.eventListeners.queryResult.addEventListener((eventType, payload) => this.eventHandler(eventType, payload));
}
init() {
super.init(this.user.data.query_results);
}
onclick(event) {
let ressourceElement = event.target.closest('tr');
if (ressourceElement === null) {return;}
let queryResultId = ressourceElement.dataset.id;
let actionButtonElement = event.target.closest('.action-button');
let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
switch (action) {
case 'delete':
let deleteModalHTML = `<div class="modal">
<div class="modal-content">
<h4>Confirm query result deletion</h4>
<p>Do you really want to delete the query result <b>${this.user.data.query_results[queryResultId].title}</b>? It will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light" href="${this.user.data.query_results[queryResultId].url}/delete"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>`;
let deleteModalParentElement = document.querySelector('#modals');
deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML);
let deleteModalElement = deleteModalParentElement.lastChild;
let deleteModal = M.Modal.init(deleteModalElement, {onCloseEnd: () => {deleteModal.destroy(); deleteModalElement.remove();}});
deleteModal.open();
break;
case 'view':
if (queryResultId !== '-1') {window.location.href = this.user.data.query_results[queryResultId].url;}
break;
default:
console.error(`Unknown action: "${action}"`);
break;
}
}
patch(patch) {
let id, match, re, valueName;
for (let operation of patch) {
switch(operation.op) {
case 'add':
// Matches the only paths that should be handled here: /jobs/{jobId}
re = /^\/query_results\/(\d+)$/;
if (re.test(operation.path)) {this.add(operation.value);}
break;
case 'remove':
// See case add ;)
re = /^\/query_results\/(\d+)$/;
if (re.test(operation.path)) {
[match, id] = operation.path.match(re);
this.remove(id);
}
break;
case 'replace':
// Matches the only paths that should be handled here: /jobs/{jobId}/{service || status || description || title}
re = /^\/query_results\/(\d+)\/(corpus_title|description|query|title)$/;
if (re.test(operation.path)) {
[match, id, valueName] = operation.path.match(re);
this.replace(id, valueName, operation.value);
}
break;
default:
break;
}
}
}
preprocessRessource(queryResult) {
return {id: queryResult.id,
corpus_title: queryResult.corpus_title,
description: queryResult.description,
query: queryResult.query,
title: queryResult.title};
}
}
QueryResultList.options = {
item: `<tr>
<td><b class="title"></b><br><i class="description"></i><br></td>
<td><span class="corpus_title"></span><br><span class="query"></span></td>
<td class="right-align">
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
</td>
</tr>`,
valueNames: [{data: ['id']}, 'corpus_title', 'description', 'query', 'title']
};
@@ -0,0 +1,99 @@
class RessourceList {
/* A wrapper class for the list.js list.
* This class is not meant to be used directly, instead it should be used as
* a base class for concrete ressource list implementations.
*/
constructor(listElement, options = {}) {
if (listElement.dataset.userId) {
if (listElement.dataset.userId in nopaque.appClient.users) {
this.user = nopaque.appClient.users[listElement.dataset.userId];
} else {
console.error(`User not found: ${listElement.dataset.userId}`);
return;
}
} else {
this.user = nopaque.appClient.users.self;
}
this.list = new List(listElement, {...RessourceList.options, ...options});
this.list.list.innerHTML = `<tr>
<td class="row" colspan="100%">
<div class="col s12">&nbsp;</div>
<div class="col s3 m2 xl1">
<div class="preloader-wrapper active">
<div class="spinner-layer spinner-green-only">
<div class="circle-clipper left">
<div class="circle"></div>
</div>
<div class="gap-patch">
<div class="circle"></div>
</div>
<div class="circle-clipper right">
<div class="circle"></div>
</div>
</div>
</div>
</div>
<div class="col s9 m6 xl5">
<span class="card-title">Waiting for data...</span>
<p>This list is not initialized yet.</p>
</div>
</td>
</tr>`;
this.list.list.style.cursor = 'pointer';
if (typeof this.onclick === 'function') {this.list.list.addEventListener('click', event => this.onclick(event));}
}
eventHandler(eventType, payload) {
switch (eventType) {
case 'init':
this.init();
break;
case 'patch':
this.patch(payload);
break;
default:
console.error(`Unknown event type: ${eventType}`);
break;
}
}
init(ressources) {
this.list.clear();
this.add(Object.values(ressources));
this.list.sort('id', {order: 'desc'});
let emptyListElementHTML = `<tr class="show-if-only-child" data-id="-1">
<td colspan="100%">
<span class="card-title"><i class="left material-icons" style="font-size: inherit;">file_download</i>Nothing here...</span>
<p>No ressource available.</p>
</td>
</tr>`;
this.list.list.insertAdjacentHTML('afterbegin', emptyListElementHTML);
}
patch(patch) {
/*
* It's not possible to generalize a patch Handler for all type of
* ressources. So this method is meant to be an interface.
*/
console.error('patch method not implemented!');
}
add(values) {
let ressources = Array.isArray(values) ? values : [values];
if (typeof this.preprocessRessource === 'function') {
ressources = ressources.map(ressource => this.preprocessRessource(ressource));
}
// Set a callback function ('() => {return;}') to force List.js perform the
// add method asynchronous: https://listjs.com/api/#add
this.list.add(ressources, () => {return;});
}
remove(id) {
this.list.remove('id', id);
}
replace(id, valueName, newValue) {
this.list.get('id', id)[0].values({[valueName]: newValue});
}
}
RessourceList.options = {page: 5, pagination: [{innerWindow: 4, outerWindow: 1}]};
@@ -0,0 +1,71 @@
class UserList extends RessourceList {
constructor(listElement, options = {}) {
super(listElement, {...UserList.options, ...options});
users = undefined;
}
init(users) {
this.users = users;
super.init(users);
}
onclick(event) {
let ressourceElement = event.target.closest('tr');
if (ressourceElement === null) {return;}
let userId = ressourceElement.dataset.id;
let actionButtonElement = event.target.closest('.action-button');
let action = (actionButtonElement === null) ? 'view' : actionButtonElement.dataset.action;
switch (action) {
case 'delete':
let deleteModalHTML = `<div class="modal">
<div class="modal-content">
<h4>Confirm user deletion</h4>
<p>Do you really want to delete the corpus <b>${this.users[userId].username}</b>? All files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light" href="/admin/users/${userId}/delete"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>`;
let deleteModalParentElement = document.querySelector('#modals');
deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML);
let deleteModalElement = deleteModalParentElement.lastChild;
let deleteModal = M.Modal.init(deleteModalElement, {onCloseEnd: () => {deleteModal.destroy(); deleteModalElement.remove();}});
deleteModal.open();
break;
case 'edit':
window.location.href = `/admin/users/${userId}/edit`;
break;
case 'view':
if (userId !== '-1') {window.location.href = `/admin/users/${userId}`;}
break;
default:
console.error(`Unknown action: ${action}`);
break;
}
}
preprocessRessource(user) {
return {id: user.id,
id_: user.id,
username: user.username,
email: user.email,
last_seen: new Date(user.last_seen * 1000).toLocaleString("en-US"),
role: user.role.name};
}
}
UserList.options = {
item: `<tr>
<td><span class="id_"></span></td>
<td><span class="username"></span></td>
<td><span class="email"></span></td>
<td><span class="last_seen"></span></td>
<td><span class="role"></span></td>
<td class="right-align">
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="edit" data-position="top" data-tooltip="Edit"><i class="material-icons">edit</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
</td>
</tr>`,
valueNames: [{data: ['id']}, 'id_', 'username', 'email', 'last_seen', 'role']
};
+250
View File
@@ -0,0 +1,250 @@
class AppClient {
constructor(currentUserId) {
if (currentUserId) {
this.socket = io({transports: ['websocket']});
this.users = {};
this.users.self = this.loadUser(currentUserId);
this.users.self.eventListeners.job.addEventListener((eventType, payload) => this.jobEventHandler(eventType, payload));
}
}
flash(message, category) {
let toast;
let toastCloseActionElement;
switch (category) {
case "corpus":
message = `<i class="left material-icons">book</i>${message}`;
break;
case "error":
message = `<i class="left material-icons error-color-text">error</i>${message}`;
break;
case "job":
message = `<i class="left nopaque-icons">J</i>${message}`;
break;
default:
message = `<i class="left material-icons">notifications</i>${message}`;
}
toast = M.toast({html: `<span>${message}</span>
<button data-action="close" class="btn-flat toast-action white-text">
<i class="material-icons">close</i>
</button>`});
toastCloseActionElement = toast.el.querySelector('.toast-action[data-action="close"]');
toastCloseActionElement.addEventListener('click', () => {toast.dismiss();});
}
jobEventHandler(eventType, payload) {
switch (eventType) {
case 'init':
break;
case 'patch':
this.jobPatch(payload);
break;
default:
console.error(`[AppClient.jobEventHandler] Unknown event type: ${eventType}`);
break;
}
}
loadUser(userId) {
if (userId in this.users) {return this.users[userId];}
let user = new User();
this.users[userId] = user;
this.socket.on(`user_${userId}_init`, msg => user.init(msg));
this.socket.on(`user_${userId}_patch`, msg => user.patch(msg));
this.socket.emit('start_user_session', userId);
return user;
}
jobPatch(patch) {
if (this.users.self.data.settings.job_status_site_notifications === 'none') {return;}
let jobStatusPatches = patch.filter(operation => operation.op === 'replace' && /^\/jobs\/(\d+)\/status$/.test(operation.path));
for (let operation of jobStatusPatches) {
let [match, jobId] = operation.path.match(/^\/jobs\/(\d+)\/status$/);
if (this.users.self.data.settings.job_status_site_notifications === "end" && !['complete', 'failed'].includes(operation.value)) {continue;}
this.flash(`[<a href="/jobs/${jobId}">${this.users.self.data.jobs[jobId].title}</a>] New status: ${operation.value}`, 'job');
}
}
}
class User {
constructor() {
this.data = undefined;
this.eventListeners = {
corpus: {
addEventListener(listener, corpusId='*') {
if (corpusId in this) {this[corpusId].push(listener);} else {this[corpusId] = [listener];}
}
},
job: {
addEventListener(listener, jobId='*') {
if (jobId in this) {this[jobId].push(listener);} else {this[jobId] = [listener];}
}
},
queryResult: {
addEventListener(listener, queryResultId='*') {
if (queryResultId in this) {this[queryResultId].push(listener);} else {this[queryResultId] = [listener];}
}
}
};
}
init(data) {
this.data = data;
for (let [corpusId, eventListeners] of Object.entries(this.eventListeners.corpus)) {
if (corpusId === '*') {
for (let eventListener of eventListeners) {eventListener('init');}
} else {
if (corpusId in this.data.corpora) {
for (let eventListener of eventListeners) {eventListener('init');}
}
}
}
for (let [jobId, eventListeners] of Object.entries(this.eventListeners.job)) {
if (jobId === '*') {
for (let eventListener of eventListeners) {eventListener('init');}
} else {
if (jobId in this.data.jobs) {
for (let eventListener of eventListeners) {eventListener('init');}
}
}
}
for (let [queryResultId, eventListeners] of Object.entries(this.eventListeners.queryResult)) {
if (queryResultId === '*') {
for (let eventListener of eventListeners) {eventListener('init');}
} else {
if (queryResultId in this.data.query_results) {
for (let eventListener of eventListeners) {eventListener('init');}
}
}
}
}
patch(patch) {
this.data = jsonpatch.apply_patch(this.data, patch);
let corporaPatch = patch.filter(operation => operation.path.startsWith("/corpora"));
if (corporaPatch.length > 0) {
for (let [corpusId, eventListeners] of Object.entries(this.eventListeners.corpus)) {
if (corpusId === '*') {
for (let eventListener of eventListeners) {eventListener('patch', corporaPatch);}
} else {
let corpusPatch = corporaPatch.filter(operation => operation.path.startsWith(`/corpora/${corpusId}`));
if (corpusPatch.length > 0) {
for (let eventListener of eventListeners) {eventListener('patch', corpusPatch);}
}
}
}
}
let jobsPatch = patch.filter(operation => operation.path.startsWith("/jobs"));
if (jobsPatch.length > 0) {
for (let [jobId, eventListeners] of Object.entries(this.eventListeners.job)) {
if (jobId === '*') {
for (let eventListener of eventListeners) {eventListener('patch', jobsPatch);}
} else {
let jobPatch = jobsPatch.filter(operation => operation.path.startsWith(`/jobs/${jobId}`));
if (jobPatch.length > 0) {
for (let eventListener of eventListeners) {eventListener('patch', jobPatch);}
}
}
}
}
let queryResultsPatch = patch.filter(operation => operation.path.startsWith("/query_results"));
if (queryResultsPatch.length > 0) {
for (let [queryResultId, eventListeners] of Object.entries(this.eventListeners.queryResult)) {
if (queryResultId === '*') {
for (let eventListener of eventListeners) {eventListener('patch', queryResultsPatch);}
} else {
let queryResultPatch = queryResultsPatch.filter(operation => operation.path.startsWith(`/query_results/${queryResultId}`));
if (queryResultPatch.length > 0) {
for (let eventListener of eventListeners) {eventListener('patch', queryResultPatch);}
}
}
}
}
}
}
/*
* The nopaque object is used as a namespace for nopaque specific functions and
* variables.
*/
var nopaque = {};
nopaque.Forms = {};
nopaque.Forms.init = function() {
var abortRequestElement, parentElement, progressElement, progressModal,
progressModalElement, request, submitElement;
for (let form of document.querySelectorAll(".nopaque-submit-form")) {
submitElement = form.querySelector('button[type="submit"]');
submitElement.addEventListener("click", function() {
for (let selectElement of form.querySelectorAll('select')) {
if (selectElement.value === "") {
parentElement = selectElement.closest(".input-field");
parentElement.querySelector(".select-dropdown").classList.add("invalid");
for (let helperTextElement of parentElement.querySelectorAll(".helper-text")) {
helperTextElement.remove();
}
parentElement.insertAdjacentHTML("beforeend", `<span class="helper-text red-text">Please select an option.</span>`);
}
}
});
request = new XMLHttpRequest();
if (form.dataset.hasOwnProperty("progressModal")) {
progressModalElement = document.getElementById(form.dataset.progressModal);
progressModal = M.Modal.getInstance(progressModalElement);
progressModal.options.dismissible = false;
abortRequestElement = progressModalElement.querySelector(".abort-request");
abortRequestElement.addEventListener("click", function() {request.abort();});
progressElement = progressModalElement.querySelector(".determinate");
}
form.addEventListener("submit", function(event) {
event.preventDefault();
var formData;
formData = new FormData(form);
// Initialize progress modal
if (progressModalElement) {
progressElement.style.width = "0%";
progressModal.open();
}
request.open("POST", window.location.href);
request.send(formData);
});
request.addEventListener("load", function(event) {
var fieldElement;
if (request.status === 201) {
window.location.href = JSON.parse(this.responseText).redirect_url;
}
if (request.status === 400) {
for (let [field, errors] of Object.entries(JSON.parse(this.responseText))) {
fieldElement = form.querySelector(`input[name$="${field}"]`).closest(".input-field");
for (let error of errors) {
fieldElement.insertAdjacentHTML("beforeend", `<span class="helper-text red-text">${error}</span>`);
}
}
if (progressModalElement) {
progressModal.close();
}
}
if (request.status === 500) {
location.reload();
}
});
if (progressModalElement) {
request.upload.addEventListener("progress", function(event) {
progressElement.style.width = Math.floor(100 * event.loaded / event.total).toString() + "%";
});
}
}
}
+7
View File
@@ -0,0 +1,7 @@
/*!
* Socket.IO v3.0.4
* (c) 2014-2020 Guillermo Rauch
* Released under the MIT License.
*/
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.io=e():t.io=e()}("undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:Function("return this")(),(function(){return function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=18)}([function(t,e,n){function r(t){if(t)return function(t){for(var e in r.prototype)t[e]=r.prototype[e];return t}(t)}t.exports=r,r.prototype.on=r.prototype.addEventListener=function(t,e){return this._callbacks=this._callbacks||{},(this._callbacks["$"+t]=this._callbacks["$"+t]||[]).push(e),this},r.prototype.once=function(t,e){function n(){this.off(t,n),e.apply(this,arguments)}return n.fn=e,this.on(t,n),this},r.prototype.off=r.prototype.removeListener=r.prototype.removeAllListeners=r.prototype.removeEventListener=function(t,e){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var n,r=this._callbacks["$"+t];if(!r)return this;if(1==arguments.length)return delete this._callbacks["$"+t],this;for(var o=0;o<r.length;o++)if((n=r[o])===e||n.fn===e){r.splice(o,1);break}return 0===r.length&&delete this._callbacks["$"+t],this},r.prototype.emit=function(t){this._callbacks=this._callbacks||{};for(var e=new Array(arguments.length-1),n=this._callbacks["$"+t],r=1;r<arguments.length;r++)e[r-1]=arguments[r];if(n){r=0;for(var o=(n=n.slice(0)).length;r<o;++r)n[r].apply(this,e)}return this},r.prototype.listeners=function(t){return this._callbacks=this._callbacks||{},this._callbacks["$"+t]||[]},r.prototype.hasListeners=function(t){return!!this.listeners(t).length}},function(t,e,n){var r=n(24),o=n(25),i=String.fromCharCode(30);t.exports={protocol:4,encodePacket:r,encodePayload:function(t,e){var n=t.length,o=new Array(n),s=0;t.forEach((function(t,c){r(t,!1,(function(t){o[c]=t,++s===n&&e(o.join(i))}))}))},decodePacket:o,decodePayload:function(t,e){for(var n=t.split(i),r=[],s=0;s<n.length;s++){var c=o(n[s],e);if(r.push(c),"error"===c.type)break}return r}}},function(t,e){t.exports="undefined"!=typeof self?self:"undefined"!=typeof window?window:Function("return this")()},function(t,e,n){function r(t){return(r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function o(t,e){for(var n=0;n<e.length;n++){var r=e[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(t,r.key,r)}}function i(t,e){return(i=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t})(t,e)}function s(t){var e=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(t){return!1}}();return function(){var n,r=a(t);if(e){var o=a(this).constructor;n=Reflect.construct(r,arguments,o)}else n=r.apply(this,arguments);return c(this,n)}}function c(t,e){return!e||"object"!==r(e)&&"function"!=typeof e?function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(t):e}function a(t){return(a=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)})(t)}var u=n(1),f=function(t){!function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),e&&i(t,e)}(a,t);var e,n,r,c=s(a);function a(t){var e;return function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,a),(e=c.call(this)).opts=t,e.query=t.query,e.readyState="",e.socket=t.socket,e}return e=a,(n=[{key:"onError",value:function(t,e){var n=new Error(t);return n.type="TransportError",n.description=e,this.emit("error",n),this}},{key:"open",value:function(){return"closed"!==this.readyState&&""!==this.readyStaLine truncated
//# sourceMappingURL=socket.io.min.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["webpack://io/webpack/universalModuleDefinition","webpack://io/webpack/bootstrap","webpack://io/./node_modules/component-emitter/index.js","webpack://io/./node_modules/engine.io-parser/lib/index.js","webpack://io/./node_modules/engine.io-client/lib/globalThis.browser.js","webpack://io/./node_modules/engine.io-client/lib/transport.js","webpack://io/./node_modules/parseqs/index.js","webpack://io/./node_modules/socket.io-parser/dist/index.js","webpack://io/./node_modules/parseuri/index.js","webpack://io/./build/manager.js","webpack://io/./node_modules/engine.io-client/lib/transports/index.js","webpack://io/./node_modules/engine.io-client/lib/xmlhttprequest.js","webpack://io/./node_modules/engine.io-client/lib/transports/polling.js","webpack://io/./node_modules/engine.io-parser/lib/commons.js","webpack://io/./node_modules/yeast/index.js","webpack://io/./node_modules/engine.io-client/lib/util.js","webpack://io/./build/socket.js","webpack://io/./node_modules/socket.io-parser/dist/is-binary.js","webpack://io/./build/on.js","webpack://io/./node_modules/component-bind/index.js","webpack://io/./build/index.js","webpack://io/./build/url.js","webpack://io/./node_modules/engine.io-client/lib/index.js","webpack://io/./node_modules/engine.io-client/lib/socket.js","webpack://io/./node_modules/has-cors/index.js","webpack://io/./node_modules/engine.io-client/lib/transports/polling-xhr.js","webpack://io/./node_modules/engine.io-parser/lib/encodePacket.browser.js","webpack://io/./node_modules/engine.io-parser/lib/decodePacket.browser.js","webpack://io/./node_modules/base64-arraybuffer/lib/base64-arraybuffer.js","webpack://io/./node_modules/engine.io-client/lib/transports/polling-jsonp.js","webpack://io/./node_modules/engine.io-client/lib/transports/websocket.js","webpack://io/./node_modules/engine.io-client/lib/transports/websocket-constructor.browser.js","webpack://io/./node_modules/socket.io-parser/dist/binary.js","webpack://io/./node_modules/backo2/index.js"],"names":["root","factory","exports","module","define","amd","self","window","global","Function","installedModules","__webpack_require__","moduleId","i","l","modules","call","m","c","d","name","getter","o","Object","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","object","property","prototype","hasOwnProperty","p","s","Emitter","obj","mixin","on","addEventListener","event","fn","this","_callbacks","push","once","off","apply","arguments","removeListener","removeAllListeners","removeEventListener","length","cb","callbacks","splice","emit","args","Array","len","slice","listeners","hasListeners","encodePacket","require","decodePacket","SEPARATOR","String","fromCharCode","protocol","encodePayload","packets","callback","encodedPackets","count","forEach","packet","encodedPacket","join","decodePayload","encodedPayload","binaryType","split","decodedPacket","type","parser","Transport","opts","query","readyState","socket","msg","desc","err","Error","description","doOpen","doClose","onClose","write","writable","data","onPacket","encode","str","encodeURIComponent","decode","qs","qry","pairs","pair","decodeURIComponent","Decoder","Encoder","PacketType","binary_1","is_binary_1","EVENT","ACK","hasBinary","encodeAsString","BINARY_EVENT","BINARY_ACK","encodeAsBinary","attachments","nsp","id","JSON","stringify","deconstruction","deconstructPacket","pack","buffers","unshift","decodeString","reconstructor","BinaryReconstructor","isBinary","base64","takeBinaryData","Number","charAt","undefined","start","buf","substring","next","payload","parse","e","tryParse","substr","isPayloadValid","finishedReconstruction","CONNECT","DISCONNECT","CONNECT_ERROR","isArray","reconPack","binData","reconstructPacket","re","parts","src","b","indexOf","replace","exec","uri","source","host","authority","ipv6uri","pathNames","path","names","queryKey","$0","$1","$2","Manager","eio","socket_1","on_1","Backoff","nsps","subs","reconnection","reconnectionAttempts","Infinity","reconnectionDelay","reconnectionDelayMax","randomizationFactor","backoff","min","max","jitter","timeout","_readyState","_parser","encoder","decoder","_autoConnect","autoConnect","open","v","_reconnection","_reconnectionAttempts","_a","_reconnectionDelay","setMin","_randomizationFactor","setJitter","_reconnectionDelayMax","setMax","_timeout","_reconnecting","attempts","reconnect","engine","skipReconnect","openSub","onopen","errorSub","cleanup","maybeReconnectOnOpen","destroy","timer","setTimeout","close","clearTimeout","add","Socket","keys","active","_close","options","subsLength","shift","reset","reason","delay","duration","onreconnect","attempt","XMLHttpRequest","XHR","JSONP","websocket","polling","xd","xs","jsonp","location","isSSL","port","hostname","secure","xdomain","xscheme","forceJSONP","hasCORS","globalThis","enablesXDR","XDomainRequest","concat","parseqs","yeast","Polling","poll","onPause","pause","total","doPoll","index","onOpen","doWriLine truncated
@@ -0,0 +1,62 @@
"LOC",
"MISC",
"ORG",
"PER",
"EVENT",
"GPE",
"LOC",
"ORG",
"PERSON",
"PRODUCT",
"CARDINAL",
"DATE",
"EVENT",
"FAC",
"GPE",
"LANGUAGE",
"LAW",
"LOC",
"MONEY",
"NORP",
"ORDINAL",
"ORG",
"PERCENT",
"PERSON",
"PRODUCT",
"QUANTITY",
"TIME",
"WORK_OF_ART",
"LOC",
"MISC",
"ORG",
"PER",
"LOC",
"MISC",
"ORG",
"PER",
"LOC",
"MISC",
"ORG",
"PER",
"CARDINAL",
"DATE",
"EVENT",
"FAC",
"GPE",
"LANGUAGE",
"LAW",
"LOC",
"MONEY",
"NORP",
"ORDINAL",
"ORG",
"PERCENT",
"PERSON",
"PRODUCT",
"QUANTITY",
"TIME",
"WORK_OF_ART",
"LOC",
"MISC",
"ORG",
"PER"
@@ -0,0 +1,329 @@
{
"$schema": "http://json-schema.org/draft-06/schema#",
"$ref": "#/definitions/NopaqueCQIPYResults",
"definitions": {
"NopaqueCQIPYResults": {
"type": "object",
"additionalProperties": false,
"properties": {
"matches": {
"type": "array",
"items": {
"$ref": "#/definitions/Match"
}
},
"cpos_lookup": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/CposLookup"
}
},
"text_lookup": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/CorpusAllText"
}
},
"match_count": {
"type": "integer"
},
"corpus_type": {
"type": "string"
},
"query": {
"type": "string"
},
"corpus_name": {
"type": "string"
},
"corpus_description": {
"type": "string"
},
"corpus_creation_date": {
"type": "string",
"format": "date-time"
},
"corpus_last_edited_date": {
"type": "string",
"format": "date-time"
},
"corpus_properties": {
"type": "array",
"items": {}
},
"corpus_size_tokens": {
"type": "integer"
},
"corpus_all_texts": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/CorpusAllText"
}
},
"corpus_analysis_date": {
"type": "string",
"format": "date-time"
},
"corpus_cqi_py_protocol_version": {
"type": "string"
},
"corpus_cqi_py_package_version": {
"type": "string"
},
"corpus_cqpserver_version": {
"type": "string"
},
"fullContext": {
"type": "boolean"
},
"cpos_ranges": {
"type": "boolean"
}
},
"required": [
"corpus_all_texts",
"corpus_analysis_date",
"corpus_cqi_py_package_version",
"corpus_cqi_py_protocol_version",
"corpus_cqpserver_version",
"corpus_creation_date",
"corpus_description",
"corpus_last_edited_date",
"corpus_name",
"corpus_properties",
"corpus_size_tokens",
"corpus_type",
"cpos_lookup",
"cpos_ranges",
"match_count",
"matches",
"query",
"text_lookup",
"fullContext"
],
"title": "NopaqueCQIPYResults"
},
"CorpusAllText": {
"type": "object",
"additionalProperties": false,
"properties": {
"address": {
"type": "string"
},
"author": {
"type": "string"
},
"booktitle": {
"type": "string"
},
"chapter": {
"type": "string"
},
"editor": {
"type": "string"
},
"institution": {
"type": "string"
},
"journal": {
"type": "string"
},
"pages": {
"type": "string"
},
"publisher": {
"type": "string"
},
"publishing_year": {
"type": "string",
"format": "integer"
},
"school": {
"type": "string"
},
"title": {
"type": "string"
},
"match_count": {
"type": "integer"
}
},
"required": [
"address",
"author",
"booktitle",
"chapter",
"editor",
"institution",
"journal",
"pages",
"publisher",
"publishing_year",
"school",
"title"
],
"title": "CorpusAllText"
},
"CposLookup": {
"type": "object",
"additionalProperties": false,
"properties": {
"word": {
"type": "string"
},
"lemma": {
"type": "string"
},
"simple_pos": {
"$ref": "#/definitions/SimplePos"
},
"pos": {
"type": "string"
},
"ner": {
"$ref": "#/definitions/Ner"
},
"text": {
"type": "integer"
},
"s": {
"type": "integer"
}
},
"required": [
"lemma",
"ner",
"pos",
"s",
"simple_pos",
"text",
"word"
],
"title": "CposLookup"
},
"Match": {
"type": "object",
"additionalProperties": false,
"properties": {
"lc": {
"type": "array",
"items": {
"type": "integer"
}
},
"c": {
"type": "array",
"items": {
"type": "integer"
}
},
"rc": {
"type": "array",
"items": {
"type": "integer"
}
}
},
"required": [
"c",
"lc",
"rc"
],
"title": "Match"
},
"Ner": {
"type": "string",
"enum": [
"NULL",
"LOC",
"MISC",
"ORG",
"PER",
"EVENT",
"GPE",
"LOC",
"ORG",
"PERSON",
"PRODUCT",
"CARDINAL",
"DATE",
"EVENT",
"FAC",
"GPE",
"LANGUAGE",
"LAW",
"LOC",
"MONEY",
"NORP",
"ORDINAL",
"ORG",
"PERCENT",
"PERSON",
"PRODUCT",
"QUANTITY",
"TIME",
"WORK_OF_ART",
"LOC",
"MISC",
"ORG",
"PER",
"LOC",
"MISC",
"ORG",
"PER",
"LOC",
"MISC",
"ORG",
"PER",
"CARDINAL",
"DATE",
"EVENT",
"FAC",
"GPE",
"LANGUAGE",
"LAW",
"LOC",
"MONEY",
"NORP",
"ORDINAL",
"ORG",
"PERCENT",
"PERSON",
"PRODUCT",
"QUANTITY",
"TIME",
"WORK_OF_ART",
"LOC",
"MISC",
"ORG",
"PER"
],
"title": "Ner"
},
"SimplePos": {
"type": "string",
"enum": [
"ADJ",
"ADP",
"ADV",
"AUX",
"CONJ",
"CCONJ",
"DET",
"INTJ",
"NOUN",
"NUM",
"PART",
"PRON",
"PROPN",
"PUNCT",
"SCONJ",
"SYM",
"VERB",
"X",
"SPACE"
],
"title": "SimplePos"
}
}
}