Implemented the basic webapp functionality.

master
Pacman Ghost 3 years ago
parent 4b3f974b56
commit 9d2495aa64
  1. 22
      .eslintrc.json
  2. 4
      .gitignore
  3. 4
      asl_rulebook2/webapp/__init__.py
  4. 79
      asl_rulebook2/webapp/content.py
  5. 30
      asl_rulebook2/webapp/globvars.py
  6. 12
      asl_rulebook2/webapp/main.py
  7. 21
      asl_rulebook2/webapp/run_server.py
  8. 40
      asl_rulebook2/webapp/static/ContentPane.js
  9. 70
      asl_rulebook2/webapp/static/MainApp.js
  10. 23
      asl_rulebook2/webapp/static/NavPane.js
  11. 83
      asl_rulebook2/webapp/static/SearchPane.js
  12. 23
      asl_rulebook2/webapp/static/SearchResult.js
  13. 80
      asl_rulebook2/webapp/static/TabbedPages.js
  14. 10
      asl_rulebook2/webapp/static/css/ContentPane.css
  15. 6
      asl_rulebook2/webapp/static/css/MainApp.css
  16. 3
      asl_rulebook2/webapp/static/css/NavPane.css
  17. 8
      asl_rulebook2/webapp/static/css/SearchPane.css
  18. 1
      asl_rulebook2/webapp/static/css/SearchResult.css
  19. 4
      asl_rulebook2/webapp/static/css/TabbedPages.css
  20. 15
      asl_rulebook2/webapp/static/css/global.css
  21. 96
      asl_rulebook2/webapp/static/growl/jquery.growl.css
  22. 311
      asl_rulebook2/webapp/static/growl/jquery.growl.js
  23. 769
      asl_rulebook2/webapp/static/split/split.js
  24. 3
      asl_rulebook2/webapp/static/split/split.min.js
  25. 16
      asl_rulebook2/webapp/static/src/MainApp.js
  26. 71
      asl_rulebook2/webapp/static/tinyemitter/tinyemitter.js
  27. 1
      asl_rulebook2/webapp/static/tinyemitter/tinyemitter.min.js
  28. 17
      asl_rulebook2/webapp/static/utils.js
  29. 30
      asl_rulebook2/webapp/templates/index.html
  30. 10
      asl_rulebook2/webapp/tests/control_tests.py
  31. 24
      asl_rulebook2/webapp/tests/control_tests_servicer.py
  32. BIN
      asl_rulebook2/webapp/tests/fixtures/simple/simple.docx
  33. 76
      asl_rulebook2/webapp/tests/fixtures/simple/simple.index
  34. BIN
      asl_rulebook2/webapp/tests/fixtures/simple/simple.pdf
  35. 15
      asl_rulebook2/webapp/tests/fixtures/simple/simple.targets
  36. 8
      asl_rulebook2/webapp/tests/proto/control_tests.proto
  37. 56
      asl_rulebook2/webapp/tests/proto/generated/control_tests_pb2.py
  38. 34
      asl_rulebook2/webapp/tests/proto/generated/control_tests_pb2_grpc.py
  39. 12
      asl_rulebook2/webapp/tests/test_basic.py
  40. 35
      asl_rulebook2/webapp/tests/test_eslint.py
  41. 37
      asl_rulebook2/webapp/tests/utils.py
  42. 18
      asl_rulebook2/webapp/utils.py

@ -0,0 +1,22 @@
{
"env": {
"browser": true,
"jquery": true
},
"extends": [
"eslint:recommended",
"plugin:vue/essential"
],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"plugins": [
"vue"
],
"globals": {
"Vue": "readable"
},
"rules": {
}
}

4
.gitignore vendored

@ -1,3 +1,7 @@
package.json
package-lock.json
node_modules/
_work_/
.vscode

@ -35,7 +35,6 @@ def _on_sigint( signum, stack ): #pylint: disable=unused-argument
shutdown_event.set()
# call any registered cleanup handlers
from asl_rulebook2.webapp import globvars #pylint: disable=cyclic-import
for handler in globvars.cleanup_handlers:
handler()
@ -78,6 +77,9 @@ else:
# load the application
import asl_rulebook2.webapp.main #pylint: disable=wrong-import-position,cyclic-import
import asl_rulebook2.webapp.content #pylint: disable=wrong-import-position,cyclic-import
from asl_rulebook2.webapp import globvars #pylint: disable=wrong-import-position,cyclic-import
app.before_request( globvars.on_request )
# install our signal handler
signal.signal( signal.SIGINT, _on_sigint )

@ -0,0 +1,79 @@
""" Manage the content documents. """
import os
import io
import glob
from flask import jsonify, send_file, url_for, abort
from asl_rulebook2.webapp import app
from asl_rulebook2.webapp.utils import change_extn, slugify
content_docs = None
# ---------------------------------------------------------------------
def load_content_docs():
"""Load the content documents from the data directory."""
# initialize
global content_docs
content_docs = {}
dname = app.config.get( "DATA_DIR" )
if not dname:
return
if not os.path.dirname( dname ):
raise RuntimeError( "Invalid data directory: {}".format( dname ) )
def get_doc( content_doc, key, fname, binary=False ):
fname = os.path.join( dname, fname )
if not os.path.isfile( fname ):
return
kwargs = {}
kwargs["mode"] = "rb" if binary else "r"
if not binary:
kwargs["encoding"] = "utf-8"
with open( fname, **kwargs ) as fp:
content_doc[ key ] = fp.read()
# load each content doc
fspec = os.path.join( dname, "*.index" )
for fname in glob.glob( fspec ):
fname = os.path.basename( fname )
title = os.path.splitext( fname )[0]
content_doc = {
"doc_id": slugify( title ),
"title": title,
}
get_doc( content_doc, "index", fname )
get_doc( content_doc, "targets", change_extn(fname,".targets") )
get_doc( content_doc, "footnotes", change_extn(fname,".footnotes") )
get_doc( content_doc, "content", change_extn(fname,".pdf"), binary=True )
content_docs[ content_doc["doc_id"] ] = content_doc
# ---------------------------------------------------------------------
@app.route( "/content-docs" )
def get_content_docs():
"""Return the available content docs."""
resp = {}
for cdoc in content_docs.values():
cdoc2 = {
"docId": cdoc["doc_id"],
"title": cdoc["title"],
}
if "content" in cdoc:
cdoc2["url"] = url_for( "get_content", doc_id=cdoc["doc_id"] )
resp[ cdoc["doc_id"] ] = cdoc2
return jsonify( resp )
# ---------------------------------------------------------------------
@app.route( "/content/<doc_id>" )
def get_content( doc_id ):
"""Return the content for the specified document."""
cdoc = content_docs.get( doc_id )
if not cdoc or "content" not in cdoc:
abort( 404 )
buf = io.BytesIO( cdoc["content"] )
return send_file( buf, mimetype="application/pdf" )

@ -1,4 +1,8 @@
""" Global variables. """
""" Global definitions. """
import threading
from flask import request
from asl_rulebook2.webapp import app
from asl_rulebook2.webapp.config.constants import APP_NAME, APP_VERSION
@ -7,6 +11,30 @@ cleanup_handlers = []
# ---------------------------------------------------------------------
_init_lock = threading.Lock()
_init_done = False
def on_request():
"""Called before each request."""
if request.path == "/control-tests":
# NOTE: The test suite calls $/control-tests to find out which port the gRPC test control service
# is running on, which is nice since we don't need to configure both ends with a predefined port.
# However, we don't want this call to trigger initialization, since the tests will often want to
# configure the remote webapp before loading the main page.
return
with _init_lock:
global _init_done
if not _init_done or (request.path == "/" and request.args.get("reload")):
try:
from asl_rulebook2.webapp.main import init_webapp
init_webapp()
finally:
# NOTE: It's important to set this, even if initialization failed, so we don't
# try to initialize again.
_init_done = True
# ---------------------------------------------------------------------
@app.context_processor
def inject_template_params():
"""Inject template parameters into Jinja2."""

@ -8,10 +8,22 @@ import logging
from flask import render_template, jsonify, abort
from asl_rulebook2.webapp import app, globvars, shutdown_event
from asl_rulebook2.webapp.content import load_content_docs
from asl_rulebook2.webapp.utils import parse_int
# ---------------------------------------------------------------------
def init_webapp():
"""Initialize the webapp.
IMPORTANT: This is called on the first Flask request, but can also be called multiple times
after that by the test suite, to reset the webapp before each test.
"""
# initialize the webapp
load_content_docs()
# ---------------------------------------------------------------------
@app.route( "/" )
def main():
"""Return the main page."""

@ -2,6 +2,9 @@
""" Run the webapp server. """
import os
import threading
import urllib.request
import time
import glob
import click
@ -12,8 +15,10 @@ from asl_rulebook2.webapp import app
@click.command()
@click.option( "--addr","-a","bind_addr", help="Webapp server address (host:port)." )
@click.option( "--data","-d","data_dir", help="Data directory." )
@click.option( "--force-init-delay", default=0, help="Force the webapp to initialize (#seconds delay)." )
@click.option( "--debug","flask_debug", is_flag=True, default=False, help="Run Flask in debug mode." )
def main( bind_addr, flask_debug ):
def main( bind_addr, data_dir, force_init_delay, flask_debug ):
"""Run the webapp server."""
# initialize
@ -30,6 +35,12 @@ def main( bind_addr, flask_debug ):
if not flask_debug:
flask_debug = app.config.get( "FLASK_DEBUG", False )
# initialize
if data_dir:
if not os.path.isdir( data_dir ):
raise RuntimeError( "Invalid data directory: {}".format( data_dir ) )
app.config["DATA_DIR"] = data_dir
# validate the configuration
if not host:
raise RuntimeError( "The server host was not set." )
@ -51,6 +62,14 @@ def main( bind_addr, flask_debug ):
files = glob.glob( fspec )
extra_files.extend( files )
# check if we should force webapp initialization
if force_init_delay > 0:
def _start_server():
time.sleep( force_init_delay )
url = "http://{}:{}/ping".format( host, port )
_ = urllib.request.urlopen( url )
threading.Thread( target=_start_server, daemon=True ).start()
# run the server
app.run( host=host, port=port, debug=flask_debug,
extra_files = extra_files

@ -0,0 +1,40 @@
import { gMainApp, gEventBus, gUrlParams } from "./MainApp.js" ;
// --------------------------------------------------------------------
gMainApp.component( "content-pane", {
props: [ "contentDocs" ],
template: `
<tabbed-pages ref="tabbedPages">
<tabbed-page v-for="doc in contentDocs" :tabId=doc.docId :caption=doc.title >
<content-doc :doc=doc />
</tabbed-page>
</tabbed-pages>`,
mounted() {
gEventBus.on( "show-content-doc", (docId) => {
this.$refs.tabbedPages.activateTab( docId ) ; // nb: tabId == docId
} ) ;
},
} ) ;
// --------------------------------------------------------------------
gMainApp.component( "content-doc", {
props: [ "doc" ],
data() { return {
noContent: gUrlParams.get( "no-content" ),
} ; },
template: `
<div class="content-doc">
<div v-if=noContent class="disabled"> Content disabled. </div>
<iframe v-else-if=doc.url :src=doc.url />
<div v-else class="disabled"> No content. </div>
</div>`,
} ) ;

@ -0,0 +1,70 @@
import { showErrorMsg } from "./utils.js" ;
// parse any URL parameters
export let gUrlParams = new URLSearchParams( window.location.search.substring(1) ) ;
// create the main application
export const gMainApp = Vue.createApp( { //eslint-disable-line no-undef
template: "<main-app />",
} ) ;
export const gEventBus = new TinyEmitter() ; //eslint-disable-line no-undef
$(document).ready( () => {
gMainApp.mount( "#main-app" ) ;
} ) ;
// --------------------------------------------------------------------
gMainApp.component( "main-app", {
data() { return {
contentDocs: [],
isLoaded: false,
} ; },
template: `
<nav-pane id="nav" />
<content-pane id="content" :contentDocs=contentDocs />
<div v-if=isLoaded id="_mainapp-loaded_" />
`,
mounted() {
// initialize the splitter
Split( [ "#nav", "#content" ], { //eslint-disable-line no-undef
direction: "horizontal",
sizes: [ 25, 75 ],
gutterSize: 2,
} ) ;
// initialze the webapp
// NOTE: We don't provide a catch handler, since each individual Promise should report
// their own errors i.e. what could we do here, other than show a generic "startup failed" error?
Promise.all( [
this.getContentDocs( this ),
] ).then( () => {
this.isLoaded = true ;
$( "#query-string" ).focus() ; // nb: because autofocus on the <input> doesn't work :-/
} ) ;
},
methods: {
getContentDocs: (self) => new Promise( (resolve, reject) => {
// get the content docs
$.getJSON( gGetContentDocsUrl, (resp) => { //eslint-disable-line no-undef
self.contentDocs = resp ;
let docIds = Object.keys( resp ) ;
if ( docIds.length > 0 ) {
Vue.nextTick( () => {
gEventBus.emit( "show-content-doc", docIds[0] ) ; // FIXME! which one do we choose?
} ) ;
}
resolve() ;
} ).fail( (xhr, status, errorMsg) => {
const msg = "Couldn't get the content docs." ;
showErrorMsg( msg + " <div class='pre'>" + errorMsg + "</div>" ) ;
reject( msg )
} ) ;
} ),
},
} ) ;

@ -0,0 +1,23 @@
import { gMainApp, gEventBus } from "./MainApp.js" ;
// --------------------------------------------------------------------
gMainApp.component( "nav-pane", {
template: `
<tabbed-pages>
<tabbed-page tabId="search" caption="Search" data-display="flex" >
<search-box id="search-box" @search=onSearch />
<search-results id="search-results" />
</tabbed-page>
</tabbed-pages>`,
methods: {
onSearch: (queryString) => {
gEventBus.emit( "search", queryString ) ;
},
},
} ) ;

@ -0,0 +1,83 @@
import { gMainApp, gEventBus } from "./MainApp.js" ;
import { IndexSearchResult } from "./SearchResult.js" ;
// --------------------------------------------------------------------
gMainApp.component( "search-panel", {
template: "<search-box /> <search-results />",
} ) ;
// --------------------------------------------------------------------
gMainApp.component( "search-box", {
data: function() { return {
queryString: "",
} ; },
template: `
<div>
<input type="text" id="query-string" @keyup=onKeyUp v-model.trim="queryString" ref="queryString" autofocus >
<button @click="$emit('search',this.queryString)" ref=submit> Go </button>
</div>`,
mounted: function() {
// initialize
$( this.$refs.queryString ).addClass( "ui-widget ui-state-default ui-corner-all" ) ;
$( this.$refs.submit ).button() ;
},
methods: {
onKeyUp: function( evt ) {
if ( evt.keyCode == 13 )
this.$refs["submit"].click() ;
}
},
} ) ;
// --------------------------------------------------------------------
gMainApp.component( "search-results", {
data() { return {
searchResults: [],
} ; },
template: `<div>
<div v-for="sr in searchResults" :key=sr.key >
<index-sr v-if="sr.srType == 'index'" :sr=sr />
<div v-else> ??? </div>
</div>
</div>`,
mounted() {
gEventBus.on( "search", this.onSearch ) ;
},
methods: {
onSearch( queryString ) {
// generate some dummy search results
let searchResults = [] ;
for ( let i=0 ; i < queryString.length ; ++i ) {
let buf = [ "Search result #" + (1+i) ] ;
let nItems = Math.floor( Math.sqrt( 100 * Math.random() ) ) - 1 ;
if ( nItems > 0 ) {
buf.push( "<ul style='padding-left:1em;'>" ) ;
for ( let j=0 ; j < nItems ; ++j )
buf.push( "<li> item " + (1+j) ) ;
buf.push( "</ul>" ) ;
}
searchResults.push(
new IndexSearchResult( i, buf.join("") )
) ;
}
this.searchResults = searchResults ;
},
},
} ) ;

@ -0,0 +1,23 @@
import { gMainApp } from "./MainApp.js" ;
// --------------------------------------------------------------------
export class IndexSearchResult {
constructor( key, content ) {
this.key = key ;
this.srType = "index" ;
this.content = content ;
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
gMainApp.component( "index-sr", {
props: [ "sr" ],
template: `
<div class="sr index-sr" v-html=sr.content />
`,
} ) ;

@ -0,0 +1,80 @@
import { gMainApp, gEventBus } from "./MainApp.js" ;
// --------------------------------------------------------------------
gMainApp.component( "tabbed-pages", {
data: function() { return {
tabs: [],
activeTabId: null,
} ; },
template: `
<div class="tabbed-pages">
<slot />
<div class="tab-strip">
<div v-for="tab in tabs" :data-tabid=tab.tabId @click=onTabClicked class="tab" v-bind:class="{'active': tab.tabId == activeTabId}" >
{{tab.caption}}
</div>
</div>
</div>`,
created() {
// FUDGE! It's nice to have the parent manage the TabbedPage's, and we just show the content
// as a slot, but that makes it tricky for us to create the tab strip, since we don't know anything
// about the tabs themselves. We work around this by having each TabbedPage emit an event when
// they mount, but since we ourself mount only after all our children have mounted, it's tricky
// for us to catch these events ($on() was remove in Vue 3 :-/). So, we emit the event on the
// global event bus, and check if they're for one of our TabbedPage's when we receive them.
gEventBus.on( "tab-loaded", (tabbedPage) => {
if ( ! tabbedPage.$el.parentNode.isSameNode( this.$el ) )
return ;
// one of our TabbedPage's has just mounted - show it in our tab strip
this.tabs.push( {
tabId: tabbedPage.tabId, caption: tabbedPage.caption
} ) ;
} ) ;
},
mounted() {
// start with the first tab activated
if ( this.tabs.length > 0 )
this.activateTab( this.tabs[0].tabId ) ;
},
methods: {
onTabClicked: function( evt ) {
// activate the selected tab
this.activateTab( evt.target.dataset.tabid ) ;
},
activateTab: function( tabId ) {
// activate the specified tab
this.activeTabId = tabId ;
$( this.$el ).find( ".tabbed-page" ).each( function() {
let displayStyle = $(this).data("display") || "block" ;
$(this).css( "display", ($(this).data("tabid") == tabId) ? displayStyle : "none" ) ;
} ) ;
},
},
} ) ;
// --------------------------------------------------------------------
gMainApp.component( "tabbed-page", {
props: [ "tabId", "caption", "isActive" ],
template: `
<div :data-tabid=tabId v-show=isActive class="tabbed-page" >
<slot />
</div>`,
mounted() {
gEventBus.emit( "tab-loaded", this ) ;
},
} ) ;

@ -0,0 +1,10 @@
#content { position: relative ; }
#content .tabbed-page { border-bottom: 1px solid #aaa ; }
#content .tab-strip { padding: 0 0.5em ; }
#content .tab-strip .tab { margin-top: -1px ; z-index: 5 ; border-radius: 0 0 5px 5px ; }
#content .tab-strip .tab { background: #f0f0f0 ; color: #808080 ; }
#content .tab-strip .tab.active { background: white ; color: #444 ; border-top: none ; }
#content .content-doc iframe { position: absolute ; width: 100% ; height: calc(100% - 35px) ; border: none ; }
#content .content-doc .disabled { margin-top: 1em ; text-align: center ; font-style: italic ; color: #888 ; }

@ -0,0 +1,6 @@
#main-app { width: 100% ; height: 100% ; display: flex ; }
/* splitter */
#nav { min-width: 300px ; }
#content { flex-grow: 1 ; min-width: 500px ; }
#main-app .gutter.gutter-horizontal { cursor: ew-resize ; background: #ccc ; }

@ -0,0 +1,3 @@
#nav { padding: 5px ; }
#nav .tabbed-page { height: calc(100% - 25px) ; padding: 2px ; }

@ -0,0 +1,8 @@
#nav .tabbed-page[data-tabid="search"] { display: flex ; flex-direction: column ; }
/* search box */
#search-box { display: flex ; }
#search-box input#query-string { margin-right: 5px ; flex-grow: 1 ; }
/* search results */
#search-results { flex-grow: -1 ; margin: 8px 0 2px 0 ; overflow-y: auto ; }

@ -0,0 +1 @@
#search-results .sr { margin: 0 10px 2px 0 ; border: 1px dotted #666 ; padding: 5px ; }

@ -0,0 +1,4 @@
.tabbed-pages { display: flex ; flex-direction: column ; }
.tabbed-pages .tabbed-page { height: calc(100% - 24px) ; }
.tabbed-pages .tab-strip { height: 24px ; display: flex ; font-size: 10pt ; font-style: italic ; }
.tabbed-pages .tab-strip .tab { border: 1px solid #aaa ; padding: 2px 5px ; margin-right: 5px ; }

@ -0,0 +1,15 @@
*/* global reset */
* { margin: 0 ; padding: 0 }
html { height: 100% ; }
body { height: 100% ; overflow: hidden ; }
/* general styling */
body { font-family: Arial, Helvetica, sans-serif ; font-size: 16px ; }
input[type="text"] { height: 22px ; padding: 0 5px ; }
button { height: 24px ; padding: 0 5px !important ; }
/* notification balloons */
.growl .growl-close { position: absolute ; top: 0 ; right: 6px ; }
.growl .growl-title { display: none ; }
.growl .pre { font-family: monospace ; }
.growl div.pre { margin: 0 0 15px 15px ; font-size: 80% ; }

@ -0,0 +1,96 @@
/* jQuery Growl
* Copyright 2015 Kevin Sylvestre
* 1.3.5
*/
.ontop, #growls-default, #growls-tl, #growls-tr, #growls-bl, #growls-br, #growls-tc, #growls-bc, #growls-cc, #growls-cl, #growls-cr {
z-index: 50000;
position: fixed; }
#growls-default {
top: 10px;
right: 10px; }
#growls-tl {
top: 10px;
left: 10px; }
#growls-tr {
top: 10px;
right: 10px; }
#growls-bl {
bottom: 10px;
left: 10px; }
#growls-br {
bottom: 10px;
right: 10px; }
#growls-tc {
top: 10px;
right: 10px;
left: 10px; }
#growls-bc {
bottom: 10px;
right: 10px;
left: 10px; }
#growls-cc {
top: 50%;
left: 50%;
margin-left: -125px; }
#growls-cl {
top: 50%;
left: 10px; }
#growls-cr {
top: 50%;
right: 10px; }
#growls-tc .growl, #growls-bc .growl {
margin-left: auto;
margin-right: auto; }
.growl {
opacity: 0.8;
filter: alpha(opacity=80);
position: relative;
border-radius: 4px;
-webkit-transition: all 0.4s ease-in-out;
-moz-transition: all 0.4s ease-in-out;
transition: all 0.4s ease-in-out; }
.growl.growl-incoming {
opacity: 0;
filter: alpha(opacity=0); }
.growl.growl-outgoing {
opacity: 0;
filter: alpha(opacity=0); }
.growl.growl-small {
width: 200px;
padding: 5px;
margin: 5px; }
.growl.growl-medium {
width: 250px;
padding: 10px;
margin: 10px; }
.growl.growl-large {
width: 300px;
padding: 15px;
margin: 15px; }
.growl.growl-default {
color: #FFF;
background: #7f8c8d; }
.growl.growl-error {
color: #FFF;
background: #C0392B; }
.growl.growl-notice {
color: #FFF;
background: #2ECC71; }
.growl.growl-warning {
color: #FFF;
background: #F39C12; }
.growl .growl-close {
cursor: pointer;
float: right;
font-size: 14px;
line-height: 18px;
font-weight: normal;
font-family: helvetica, verdana, sans-serif; }
.growl .growl-title {
font-size: 18px;
line-height: 24px; }
.growl .growl-message {
font-size: 14px;
line-height: 18px; }

@ -0,0 +1,311 @@
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
// Generated by CoffeeScript 2.1.0
(function () {
/*
jQuery Growl
Copyright 2015 Kevin Sylvestre
1.3.5
*/
"use strict";
var $, Animation, Growl;
$ = jQuery;
Animation = function () {
var Animation = function () {
function Animation() {
_classCallCheck(this, Animation);
}
_createClass(Animation, null, [{
key: "transition",
value: function transition($el) {
var el, ref, result, type;
el = $el[0];
ref = this.transitions;
for (type in ref) {
result = ref[type];
if (el.style[type] != null) {
return result;
}
}
}
}]);
return Animation;
}();
;
Animation.transitions = {
"webkitTransition": "webkitTransitionEnd",
"mozTransition": "mozTransitionEnd",
"oTransition": "oTransitionEnd",
"transition": "transitionend"
};
return Animation;
}();
Growl = function () {
var Growl = function () {
_createClass(Growl, null, [{
key: "growl",
value: function growl() {
var settings = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
return new Growl(settings);
}
}]);
function Growl() {
var settings = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
_classCallCheck(this, Growl);
this.render = this.render.bind(this);
this.bind = this.bind.bind(this);
this.unbind = this.unbind.bind(this);
this.mouseEnter = this.mouseEnter.bind(this);
this.mouseLeave = this.mouseLeave.bind(this);
this.click = this.click.bind(this);
this.close = this.close.bind(this);
this.cycle = this.cycle.bind(this);
this.waitAndDismiss = this.waitAndDismiss.bind(this);
this.present = this.present.bind(this);
this.dismiss = this.dismiss.bind(this);
this.remove = this.remove.bind(this);
this.animate = this.animate.bind(this);
this.$growls = this.$growls.bind(this);
this.$growl = this.$growl.bind(this);
this.html = this.html.bind(this);
this.content = this.content.bind(this);
this.container = this.container.bind(this);
this.settings = $.extend({}, Growl.settings, settings);
this.initialize(this.settings.location);
this.render();
}
_createClass(Growl, [{
key: "initialize",
value: function initialize(location) {
var id;
id = 'growls-' + location;
return $('body:not(:has(#' + id + '))').append('<div id="' + id + '" />');
}
}, {
key: "render",
value: function render() {
var $growl;
$growl = this.$growl();
this.$growls(this.settings.location).append($growl);
if (this.settings.fixed) {
this.present();
} else {
this.cycle();
}
}
}, {
key: "bind",
value: function bind() {
var $growl = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.$growl();
$growl.on("click", this.click);
if (this.settings.delayOnHover) {
$growl.on("mouseenter", this.mouseEnter);
$growl.on("mouseleave", this.mouseLeave);
}
return $growl.on("contextmenu", this.close).find("." + this.settings.namespace + "-close").on("click", this.close);
}
}, {
key: "unbind",
value: function unbind() {
var $growl = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.$growl();
$growl.off("click", this.click);
if (this.settings.delayOnHover) {
$growl.off("mouseenter", this.mouseEnter);
$growl.off("mouseleave", this.mouseLeave);
}
return $growl.off("contextmenu", this.close).find("." + this.settings.namespace + "-close").off("click", this.close);
}
}, {
key: "mouseEnter",
value: function mouseEnter(event) {
var $growl;
$growl = this.$growl();
return $growl.stop(true, true);
}
}, {
key: "mouseLeave",
value: function mouseLeave(event) {
return this.waitAndDismiss();
}
}, {
key: "click",
value: function click(event) {
if (this.settings.url != null) {
event.preventDefault();
event.stopPropagation();
return window.open(this.settings.url);
}
}
}, {
key: "close",
value: function close(event) {
var $growl;
event.preventDefault();
event.stopPropagation();
$growl = this.$growl();
return $growl.stop().queue(this.dismiss).queue(this.remove);
}
}, {
key: "cycle",
value: function cycle() {
var $growl;
$growl = this.$growl();
return $growl.queue(this.present).queue(this.waitAndDismiss());
}
}, {
key: "waitAndDismiss",
value: function waitAndDismiss() {
var $growl;
$growl = this.$growl();
return $growl.delay(this.settings.duration).queue(this.dismiss).queue(this.remove);
}
}, {
key: "present",
value: function present(callback) {
var $growl;
$growl = this.$growl();
this.bind($growl);
return this.animate($growl, this.settings.namespace + "-incoming", 'out', callback);
}
}, {
key: "dismiss",
value: function dismiss(callback) {
var $growl;
$growl = this.$growl();
this.unbind($growl);
return this.animate($growl, this.settings.namespace + "-outgoing", 'in', callback);
}
}, {
key: "remove",
value: function remove(callback) {
this.$growl().remove();
return typeof callback === "function" ? callback() : void 0;
}
}, {
key: "animate",
value: function animate($element, name) {
var direction = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'in';
var callback = arguments[3];
var transition;
transition = Animation.transition($element);
$element[direction === 'in' ? 'removeClass' : 'addClass'](name);
$element.offset().position;
$element[direction === 'in' ? 'addClass' : 'removeClass'](name);
if (callback == null) {
return;
}
if (transition != null) {
$element.one(transition, callback);
} else {
callback();
}
}
}, {
key: "$growls",
value: function $growls(location) {
var base;
if (this.$_growls == null) {
this.$_growls = [];
}
return (base = this.$_growls)[location] != null ? base[location] : base[location] = $('#growls-' + location);
}
}, {
key: "$growl",
value: function $growl() {
return this.$_growl != null ? this.$_growl : this.$_growl = $(this.html());
}
}, {
key: "html",
value: function html() {
return this.container(this.content());
}
}, {
key: "content",
value: function content() {
return "<div class='" + this.settings.namespace + "-close'>" + this.settings.close + "</div>\n<div class='" + this.settings.namespace + "-title'>" + this.settings.title + "</div>\n<div class='" + this.settings.namespace + "-message'>" + this.settings.message + "</div>";
}
}, {
key: "container",
value: function container(content) {
return "<div class='" + this.settings.namespace + " " + this.settings.namespace + "-" + this.settings.style + " " + this.settings.namespace + "-" + this.settings.size + "'>\n " + content + "\n</div>";
}
}]);
return Growl;
}();
;
Growl.settings = {
namespace: 'growl',
duration: 3200,
close: "&#215;",
location: "default",
style: "default",
size: "medium",
delayOnHover: true
};
return Growl;
}();
this.Growl = Growl;
$.growl = function () {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
return Growl.growl(options);
};
$.growl.error = function () {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var settings;
settings = {
title: "Error!",
style: "error"
};
return $.growl($.extend(settings, options));
};
$.growl.notice = function () {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var settings;
settings = {
title: "Notice!",
style: "notice"
};
return $.growl($.extend(settings, options));
};
$.growl.warning = function () {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var settings;
settings = {
title: "Warning!",
style: "warning"
};
return $.growl($.extend(settings, options));
};
}).call(this);

@ -0,0 +1,769 @@
/*! Split.js - v1.6.2 */
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.Split = factory());
}(this, (function () { 'use strict';
// The programming goals of Split.js are to deliver readable, understandable and
// maintainable code, while at the same time manually optimizing for tiny minified file size,
// browser compatibility without additional requirements
// and very few assumptions about the user's page layout.
var global = typeof window !== 'undefined' ? window : null;
var ssr = global === null;
var document = !ssr ? global.document : undefined;
// Save a couple long function names that are used frequently.
// This optimization saves around 400 bytes.
var addEventListener = 'addEventListener';
var removeEventListener = 'removeEventListener';
var getBoundingClientRect = 'getBoundingClientRect';
var gutterStartDragging = '_a';
var aGutterSize = '_b';
var bGutterSize = '_c';
var HORIZONTAL = 'horizontal';
var NOOP = function () { return false; };
// Helper function determines which prefixes of CSS calc we need.
// We only need to do this once on startup, when this anonymous function is called.
//
// Tests -webkit, -moz and -o prefixes. Modified from StackOverflow:
// http://stackoverflow.com/questions/16625140/js-feature-detection-to-detect-the-usage-of-webkit-calc-over-calc/16625167#16625167
var calc = ssr
? 'calc'
: ((['', '-webkit-', '-moz-', '-o-']
.filter(function (prefix) {
var el = document.createElement('div');
el.style.cssText = "width:" + prefix + "calc(9px)";
return !!el.style.length
})
.shift()) + "calc");
// Helper function checks if its argument is a string-like type
var isString = function (v) { return typeof v === 'string' || v instanceof String; };
// Helper function allows elements and string selectors to be used
// interchangeably. In either case an element is returned. This allows us to
// do `Split([elem1, elem2])` as well as `Split(['#id1', '#id2'])`.
var elementOrSelector = function (el) {
if (isString(el)) {
var ele = document.querySelector(el);
if (!ele) {
throw new Error(("Selector " + el + " did not match a DOM element"))
}
return ele
}
return el
};
// Helper function gets a property from the properties object, with a default fallback
var getOption = function (options, propName, def) {
var value = options[propName];
if (value !== undefined) {
return value
}
return def
};
var getGutterSize = function (gutterSize, isFirst, isLast, gutterAlign) {
if (isFirst) {
if (gutterAlign === 'end') {
return 0
}
if (gutterAlign === 'center') {
return gutterSize / 2
}
} else if (isLast) {
if (gutterAlign === 'start') {
return 0
}
if (gutterAlign === 'center') {
return gutterSize / 2
}
}
return gutterSize
};
// Default options
var defaultGutterFn = function (i, gutterDirection) {
var gut = document.createElement('div');
gut.className = "gutter gutter-" + gutterDirection;
return gut
};
var defaultElementStyleFn = function (dim, size, gutSize) {
var style = {};
if (!isString(size)) {
style[dim] = calc + "(" + size + "% - " + gutSize + "px)";
} else {
style[dim] = size;
}
return style
};
var defaultGutterStyleFn = function (dim, gutSize) {
var obj;
return (( obj = {}, obj[dim] = (gutSize + "px"), obj ));
};
// The main function to initialize a split. Split.js thinks about each pair
// of elements as an independant pair. Dragging the gutter between two elements
// only changes the dimensions of elements in that pair. This is key to understanding
// how the following functions operate, since each function is bound to a pair.
//
// A pair object is shaped like this:
//
// {
// a: DOM element,
// b: DOM element,
// aMin: Number,
// bMin: Number,
// dragging: Boolean,
// parent: DOM element,
// direction: 'horizontal' | 'vertical'
// }
//
// The basic sequence:
//
// 1. Set defaults to something sane. `options` doesn't have to be passed at all.
// 2. Initialize a bunch of strings based on the direction we're splitting.
// A lot of the behavior in the rest of the library is paramatized down to
// rely on CSS strings and classes.
// 3. Define the dragging helper functions, and a few helpers to go with them.
// 4. Loop through the elements while pairing them off. Every pair gets an
// `pair` object and a gutter.
// 5. Actually size the pair elements, insert gutters and attach event listeners.
var Split = function (idsOption, options) {
if ( options === void 0 ) options = {};
if (ssr) { return {} }
var ids = idsOption;
var dimension;
var clientAxis;
var position;
var positionEnd;
var clientSize;
var elements;
// Allow HTMLCollection to be used as an argument when supported
if (Array.from) {
ids = Array.from(ids);
}
// All DOM elements in the split should have a common parent. We can grab
// the first elements parent and hope users read the docs because the
// behavior will be whacky otherwise.
var firstElement = elementOrSelector(ids[0]);
var parent = firstElement.parentNode;
var parentStyle = getComputedStyle ? getComputedStyle(parent) : null;
var parentFlexDirection = parentStyle ? parentStyle.flexDirection : null;
// Set default options.sizes to equal percentages of the parent element.
var sizes = getOption(options, 'sizes') || ids.map(function () { return 100 / ids.length; });
// Standardize minSize to an array if it isn't already. This allows minSize
// to be passed as a number.
var minSize = getOption(options, 'minSize', 100);
var minSizes = Array.isArray(minSize) ? minSize : ids.map(function () { return minSize; });
// Get other options
var expandToMin = getOption(options, 'expandToMin', false);
var gutterSize = getOption(options, 'gutterSize', 10);
var gutterAlign = getOption(options, 'gutterAlign', 'center');
var snapOffset = getOption(options, 'snapOffset', 30);
var dragInterval = getOption(options, 'dragInterval', 1);
var direction = getOption(options, 'direction', HORIZONTAL);
var cursor = getOption(
options,
'cursor',
direction === HORIZONTAL ? 'col-resize' : 'row-resize'
);
var gutter = getOption(options, 'gutter', defaultGutterFn);
var elementStyle = getOption(
options,
'elementStyle',
defaultElementStyleFn
);
var gutterStyle = getOption(options, 'gutterStyle', defaultGutterStyleFn);
// 2. Initialize a bunch of strings based on the direction we're splitting.
// A lot of the behavior in the rest of the library is paramatized down to
// rely on CSS strings and classes.
if (direction === HORIZONTAL) {
dimension = 'width';
clientAxis = 'clientX';
position = 'left';
positionEnd = 'right';
clientSize = 'clientWidth';
} else if (direction === 'vertical') {
dimension = 'height';
clientAxis = 'clientY';
position = 'top';
positionEnd = 'bottom';
clientSize = 'clientHeight';
}
// 3. Define the dragging helper functions, and a few helpers to go with them.
// Each helper is bound to a pair object that contains its metadata. This
// also makes it easy to store references to listeners that that will be
// added and removed.
//
// Even though there are no other functions contained in them, aliasing
// this to self saves 50 bytes or so since it's used so frequently.
//
// The pair object saves metadata like dragging state, position and
// event listener references.
function setElementSize(el, size, gutSize, i) {
// Split.js allows setting sizes via numbers (ideally), or if you must,
// by string, like '300px'. This is less than ideal, because it breaks
// the fluid layout that `calc(% - px)` provides. You're on your own if you do that,
// make sure you calculate the gutter size by hand.
var style = elementStyle(dimension, size, gutSize, i);
Object.keys(style).forEach(function (prop) {
// eslint-disable-next-line no-param-reassign
el.style[prop] = style[prop];
});
}
function setGutterSize(gutterElement, gutSize, i) {
var style = gutterStyle(dimension, gutSize, i);
Object.keys(style).forEach(function (prop) {
// eslint-disable-next-line no-param-reassign
gutterElement.style[prop] = style[prop];
});
}
function getSizes() {
return elements.map(function (element) { return element.size; })
}
// Supports touch events, but not multitouch, so only the first
// finger `touches[0]` is counted.
function getMousePosition(e) {
if ('touches' in e) { return e.touches[0][clientAxis] }
return e[clientAxis]
}
// Actually adjust the size of elements `a` and `b` to `offset` while dragging.
// calc is used to allow calc(percentage + gutterpx) on the whole split instance,
// which allows the viewport to be resized without additional logic.
// Element a's size is the same as offset. b's size is total size - a size.
// Both sizes are calculated from the initial parent percentage,
// then the gutter size is subtracted.
function adjust(offset) {
var a = elements[this.a];
var b = elements[this.b];
var percentage = a.size + b.size;
a.size = (offset / this.size) * percentage;
b.size = percentage - (offset / this.size) * percentage;
setElementSize(a.element, a.size, this[aGutterSize], a.i);
setElementSize(b.element, b.size, this[bGutterSize], b.i);
}
// drag, where all the magic happens. The logic is really quite simple:
//
// 1. Ignore if the pair is not dragging.
// 2. Get the offset of the event.
// 3. Snap offset to min if within snappable range (within min + snapOffset).
// 4. Actually adjust each element in the pair to offset.
//
// ---------------------------------------------------------------------
// | | <- a.minSize || b.minSize -> | |
// | | | <- this.snapOffset || this.snapOffset -> | | |
// | | | || | | |
// | | | || | | |
// ---------------------------------------------------------------------
// | <- this.start this.size -> |
function drag(e) {
var offset;
var a = elements[this.a];
var b = elements[this.b];
if (!this.dragging) { return }
// Get the offset of the event from the first side of the
// pair `this.start`. Then offset by the initial position of the
// mouse compared to the gutter size.
offset =
getMousePosition(e) -
this.start +
(this[aGutterSize] - this.dragOffset);
if (dragInterval > 1) {
offset = Math.round(offset / dragInterval) * dragInterval;
}
// If within snapOffset of min or max, set offset to min or max.
// snapOffset buffers a.minSize and b.minSize, so logic is opposite for both.
// Include the appropriate gutter sizes to prevent overflows.
if (offset <= a.minSize + snapOffset + this[aGutterSize]) {
offset = a.minSize + this[aGutterSize];
} else if (
offset >=
this.size - (b.minSize + snapOffset + this[bGutterSize])
) {
offset = this.size - (b.minSize + this[bGutterSize]);
}
// Actually adjust the size.
adjust.call(this, offset);
// Call the drag callback continously. Don't do anything too intensive
// in this callback.
getOption(options, 'onDrag', NOOP)(getSizes());
}
// Cache some important sizes when drag starts, so we don't have to do that
// continously:
//
// `size`: The total size of the pair. First + second + first gutter + second gutter.
// `start`: The leading side of the first element.
//
// ------------------------------------------------
// | aGutterSize -> ||| |
// | ||| |
// | ||| |
// | ||| <- bGutterSize |
// ------------------------------------------------
// | <- start size -> |
function calculateSizes() {
// Figure out the parent size minus padding.
var a = elements[this.a].element;
var b = elements[this.b].element;
var aBounds = a[getBoundingClientRect]();
var bBounds = b[getBoundingClientRect]();
this.size =
aBounds[dimension] +
bBounds[dimension] +
this[aGutterSize] +
this[bGutterSize];
this.start = aBounds[position];
this.end = aBounds[positionEnd];
}
function innerSize(element) {
// Return nothing if getComputedStyle is not supported (< IE9)
// Or if parent element has no layout yet
if (!getComputedStyle) { return null }
var computedStyle = getComputedStyle(element);
if (!computedStyle) { return null }
var size = element[clientSize];
if (size === 0) { return null }
if (direction === HORIZONTAL) {
size -=
parseFloat(computedStyle.paddingLeft) +
parseFloat(computedStyle.paddingRight);
} else {
size -=
parseFloat(computedStyle.paddingTop) +
parseFloat(computedStyle.paddingBottom);
}
return size
}
// When specifying percentage sizes that are less than the computed
// size of the element minus the gutter, the lesser percentages must be increased
// (and decreased from the other elements) to make space for the pixels
// subtracted by the gutters.
function trimToMin(sizesToTrim) {
// Try to get inner size of parent element.
// If it's no supported, return original sizes.
var parentSize = innerSize(parent);
if (parentSize === null) {
return sizesToTrim
}
if (minSizes.reduce(function (a, b) { return a + b; }, 0) > parentSize) {
return sizesToTrim
}
// Keep track of the excess pixels, the amount of pixels over the desired percentage
// Also keep track of the elements with pixels to spare, to decrease after if needed
var excessPixels = 0;
var toSpare = [];
var pixelSizes = sizesToTrim.map(function (size, i) {
// Convert requested percentages to pixel sizes
var pixelSize = (parentSize * size) / 100;
var elementGutterSize = getGutterSize(
gutterSize,
i === 0,
i === sizesToTrim.length - 1,
gutterAlign
);
var elementMinSize = minSizes[i] + elementGutterSize;
// If element is too smal, increase excess pixels by the difference
// and mark that it has no pixels to spare
if (pixelSize < elementMinSize) {
excessPixels += elementMinSize - pixelSize;
toSpare.push(0);
return elementMinSize
}
// Otherwise, mark the pixels it has to spare and return it's original size
toSpare.push(pixelSize - elementMinSize);
return pixelSize
});
// If nothing was adjusted, return the original sizes
if (excessPixels === 0) {
return sizesToTrim
}
return pixelSizes.map(function (pixelSize, i) {
var newPixelSize = pixelSize;
// While there's still pixels to take, and there's enough pixels to spare,
// take as many as possible up to the total excess pixels
if (excessPixels > 0 && toSpare[i] - excessPixels > 0) {
var takenPixels = Math.min(
excessPixels,
toSpare[i] - excessPixels
);
// Subtract the amount taken for the next iteration
excessPixels -= takenPixels;
newPixelSize = pixelSize - takenPixels;
}
// Return the pixel size adjusted as a percentage
return (newPixelSize / parentSize) * 100
})
}
// stopDragging is very similar to startDragging in reverse.
function stopDragging() {
var self = this;
var a = elements[self.a].element;
var b = elements[self.b].element;
if (self.dragging) {
getOption(options, 'onDragEnd', NOOP)(getSizes());
}
self.dragging = false;
// Remove the stored event listeners. This is why we store them.
global[removeEventListener]('mouseup', self.stop);
global[removeEventListener]('touchend', self.stop);
global[removeEventListener]('touchcancel', self.stop);
global[removeEventListener]('mousemove', self.move);
global[removeEventListener]('touchmove', self.move);
// Clear bound function references
self.stop = null;
self.move = null;
a[removeEventListener]('selectstart', NOOP);
a[removeEventListener]('dragstart', NOOP);
b[removeEventListener]('selectstart', NOOP);
b[removeEventListener]('dragstart', NOOP);
a.style.userSelect = '';
a.style.webkitUserSelect = '';
a.style.MozUserSelect = '';
a.style.pointerEvents = '';
b.style.userSelect = '';
b.style.webkitUserSelect = '';
b.style.MozUserSelect = '';
b.style.pointerEvents = '';
self.gutter.style.cursor = '';
self.parent.style.cursor = '';
document.body.style.cursor = '';
}
// startDragging calls `calculateSizes` to store the inital size in the pair object.
// It also adds event listeners for mouse/touch events,
// and prevents selection while dragging so avoid the selecting text.
function startDragging(e) {
// Right-clicking can't start dragging.
if ('button' in e && e.button !== 0) {
return
}
// Alias frequently used variables to save space. 200 bytes.
var self = this;
var a = elements[self.a].element;
var b = elements[self.b].element;
// Call the onDragStart callback.
if (!self.dragging) {
getOption(options, 'onDragStart', NOOP)(getSizes());
}
// Don't actually drag the element. We emulate that in the drag function.
e.preventDefault();
// Set the dragging property of the pair object.
self.dragging = true;
// Create two event listeners bound to the same pair object and store
// them in the pair object.
self.move = drag.bind(self);
self.stop = stopDragging.bind(self);
// All the binding. `window` gets the stop events in case we drag out of the elements.
global[addEventListener]('mouseup', self.stop);
global[addEventListener]('touchend', self.stop);
global[addEventListener]('touchcancel', self.stop);
global[addEventListener]('mousemove', self.move);
global[addEventListener]('touchmove', self.move);
// Disable selection. Disable!
a[addEventListener]('selectstart', NOOP);
a[addEventListener]('dragstart', NOOP);
b[addEventListener]('selectstart', NOOP);
b[addEventListener]('dragstart', NOOP);
a.style.userSelect = 'none';
a.style.webkitUserSelect = 'none';
a.style.MozUserSelect = 'none';
a.style.pointerEvents = 'none';
b.style.userSelect = 'none';
b.style.webkitUserSelect = 'none';
b.style.MozUserSelect = 'none';
b.style.pointerEvents = 'none';
// Set the cursor at multiple levels
self.gutter.style.cursor = cursor;
self.parent.style.cursor = cursor;
document.body.style.cursor = cursor;
// Cache the initial sizes of the pair.
calculateSizes.call(self);
// Determine the position of the mouse compared to the gutter
self.dragOffset = getMousePosition(e) - self.end;
}
// adjust sizes to ensure percentage is within min size and gutter.
sizes = trimToMin(sizes);
// 5. Create pair and element objects. Each pair has an index reference to
// elements `a` and `b` of the pair (first and second elements).
// Loop through the elements while pairing them off. Every pair gets a
// `pair` object and a gutter.
//
// Basic logic:
//
// - Starting with the second element `i > 0`, create `pair` objects with
// `a = i - 1` and `b = i`
// - Set gutter sizes based on the _pair_ being first/last. The first and last
// pair have gutterSize / 2, since they only have one half gutter, and not two.
// - Create gutter elements and add event listeners.
// - Set the size of the elements, minus the gutter sizes.
//
// -----------------------------------------------------------------------
// | i=0 | i=1 | i=2 | i=3 |
// | | | | |
// | pair 0 pair 1 pair 2 |
// | | | | |
// -----------------------------------------------------------------------
var pairs = [];
elements = ids.map(function (id, i) {
// Create the element object.
var element = {
element: elementOrSelector(id),
size: sizes[i],
minSize: minSizes[i],
i: i,
};
var pair;
if (i > 0) {
// Create the pair object with its metadata.
pair = {
a: i - 1,
b: i,
dragging: false,
direction: direction,
parent: parent,
};
pair[aGutterSize] = getGutterSize(
gutterSize,
i - 1 === 0,
false,
gutterAlign
);
pair[bGutterSize] = getGutterSize(
gutterSize,
false,
i === ids.length - 1,
gutterAlign
);
// if the parent has a reverse flex-direction, switch the pair elements.
if (
parentFlexDirection === 'row-reverse' ||
parentFlexDirection === 'column-reverse'
) {
var temp = pair.a;
pair.a = pair.b;
pair.b = temp;
}
}
// Determine the size of the current element. IE8 is supported by
// staticly assigning sizes without draggable gutters. Assigns a string
// to `size`.
//
// Create gutter elements for each pair.
if (i > 0) {
var gutterElement = gutter(i, direction, element.element);
setGutterSize(gutterElement, gutterSize, i);
// Save bound event listener for removal later
pair[gutterStartDragging] = startDragging.bind(pair);
// Attach bound event listener
gutterElement[addEventListener](
'mousedown',
pair[gutterStartDragging]
);
gutterElement[addEventListener](
'touchstart',
pair[gutterStartDragging]
);
parent.insertBefore(gutterElement, element.element);
pair.gutter = gutterElement;
}
setElementSize(
element.element,
element.size,
getGutterSize(
gutterSize,
i === 0,
i === ids.length - 1,
gutterAlign
),
i
);
// After the first iteration, and we have a pair object, append it to the
// list of pairs.
if (i > 0) {
pairs.push(pair);
}
return element
});
function adjustToMin(element) {
var isLast = element.i === pairs.length;
var pair = isLast ? pairs[element.i - 1] : pairs[element.i];
calculateSizes.call(pair);
var size = isLast
? pair.size - element.minSize - pair[bGutterSize]
: element.minSize + pair[aGutterSize];
adjust.call(pair, size);
}
elements.forEach(function (element) {
var computedSize = element.element[getBoundingClientRect]()[dimension];
if (computedSize < element.minSize) {
if (expandToMin) {
adjustToMin(element);
} else {
// eslint-disable-next-line no-param-reassign
element.minSize = computedSize;
}
}
});
function setSizes(newSizes) {
var trimmed = trimToMin(newSizes);
trimmed.forEach(function (newSize, i) {
if (i > 0) {
var pair = pairs[i - 1];
var a = elements[pair.a];
var b = elements[pair.b];
a.size = trimmed[i - 1];
b.size = newSize;
setElementSize(a.element, a.size, pair[aGutterSize], a.i);
setElementSize(b.element, b.size, pair[bGutterSize], b.i);
}
});
}
function destroy(preserveStyles, preserveGutter) {
pairs.forEach(function (pair) {
if (preserveGutter !== true) {
pair.parent.removeChild(pair.gutter);
} else {
pair.gutter[removeEventListener](
'mousedown',
pair[gutterStartDragging]
);
pair.gutter[removeEventListener](
'touchstart',
pair[gutterStartDragging]
);
}
if (preserveStyles !== true) {
var style = elementStyle(
dimension,
pair.a.size,
pair[aGutterSize]
);
Object.keys(style).forEach(function (prop) {
elements[pair.a].element.style[prop] = '';
elements[pair.b].element.style[prop] = '';
});
}
});
}
return {
setSizes: setSizes,
getSizes: getSizes,
collapse: function collapse(i) {
adjustToMin(elements[i]);
},
destroy: destroy,
parent: parent,
pairs: pairs,
}
};
return Split;
})));

File diff suppressed because one or more lines are too long

@ -1,16 +0,0 @@
gMainApp.component( "main-app", {
data() { return {
isLoaded: false,
} ; },
template: `
<div> Hello, world! </div>
<div v-if="isLoaded" id="_mainapp-loaded_" />
`,
mounted() {
this.isLoaded = true ;
},
} ) ;

@ -0,0 +1,71 @@
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.TinyEmitter = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
function E () {
// Keep this empty so it's easier to inherit from
// (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)
}
E.prototype = {
on: function (name, callback, ctx) {
var e = this.e || (this.e = {});
(e[name] || (e[name] = [])).push({
fn: callback,
ctx: ctx
});
return this;
},
once: function (name, callback, ctx) {
var self = this;
function listener () {
self.off(name, listener);
callback.apply(ctx, arguments);
};
listener._ = callback
return this.on(name, listener, ctx);
},
emit: function (name) {
var data = [].slice.call(arguments, 1);
var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
var i = 0;
var len = evtArr.length;
for (i; i < len; i++) {
evtArr[i].fn.apply(evtArr[i].ctx, data);
}
return this;
},
off: function (name, callback) {
var e = this.e || (this.e = {});
var evts = e[name];
var liveEvents = [];
if (evts && callback) {
for (var i = 0, len = evts.length; i < len; i++) {
if (evts[i].fn !== callback && evts[i].fn._ !== callback)
liveEvents.push(evts[i]);
}
}
// Remove event from queue to prevent memory leak
// Suggested by https://github.com/lazd
// Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910
(liveEvents.length)
? e[name] = liveEvents
: delete e[name];
return this;
}
};
module.exports = E;
module.exports.TinyEmitter = E;
},{}]},{},[1])(1)
});

@ -0,0 +1 @@
(function(e){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=e()}else if(typeof define==="function"&&define.amd){define([],e)}else{var n;if(typeof window!=="undefined"){n=window}else if(typeof global!=="undefined"){n=global}else if(typeof self!=="undefined"){n=self}else{n=this}n.TinyEmitter=e()}})(function(){var e,n,t;return function r(e,n,t){function i(o,u){if(!n[o]){if(!e[o]){var s=typeof require=="function"&&require;if(!u&&s)return s(o,!0);if(f)return f(o,!0);var a=new Error("Cannot find module '"+o+"'");throw a.code="MODULE_NOT_FOUND",a}var l=n[o]={exports:{}};e[o][0].call(l.exports,function(n){var t=e[o][1][n];return i(t?t:n)},l,l.exports,r,e,n,t)}return n[o].exports}var f=typeof require=="function"&&require;for(var o=0;o<t.length;o++)i(t[o]);return i}({1:[function(e,n,t){function r(){}r.prototype={on:function(e,n,t){var r=this.e||(this.e={});(r[e]||(r[e]=[])).push({fn:n,ctx:t});return this},once:function(e,n,t){var r=this;function i(){r.off(e,i);n.apply(t,arguments)}i._=n;return this.on(e,i,t)},emit:function(e){var n=[].slice.call(arguments,1);var t=((this.e||(this.e={}))[e]||[]).slice();var r=0;var i=t.length;for(r;r<i;r++){t[r].fn.apply(t[r].ctx,n)}return this},off:function(e,n){var t=this.e||(this.e={});var r=t[e];var i=[];if(r&&n){for(var f=0,o=r.length;f<o;f++){if(r[f].fn!==n&&r[f].fn._!==n)i.push(r[f])}}i.length?t[e]=i:delete t[e];return this}};n.exports=r;n.exports.TinyEmitter=r},{}]},{},[1])(1)});

@ -0,0 +1,17 @@
export function showInfoMsg( msg ) { _doShowNotificationMsg( "notice", msg ) ; }
export function showWarningMsg( msg ) { _doShowNotificationMsg( "warning", msg ) ; }
export function showErrorMsg( msg ) { _doShowNotificationMsg( "error", msg ) ; }
function _doShowNotificationMsg( msgType, msg )
{
// show the notification message
$.growl( {
style: msgType,
title: null,
message: msg,
location: "br",
duration: (msgType == "warning") ? 15*1000 : 5*1000,
fixed: (msgType == "error"),
} ) ;
}

@ -8,6 +8,14 @@
<link rel="stylesheet" type="text/css" href="{{ url_for( 'static',
filename = 'jquery-ui/jquery-ui' + WEB_DEBUG_MIN + '.css'
) }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for( 'static', filename='growl/jquery.growl.css' ) }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for( 'static', filename='css/global.css' ) }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for( 'static', filename='css/MainApp.css' ) }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for( 'static', filename='css/NavPane.css' ) }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for( 'static', filename='css/SearchPane.css' ) }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for( 'static', filename='css/SearchResult.css' ) }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for( 'static', filename='css/ContentPane.css' ) }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for( 'static', filename='css/TabbedPages.css' ) }}" />
</head>
<body>
@ -25,14 +33,24 @@
<script src="{{ url_for( 'static',
filename = 'jquery-ui/jquery-ui' + WEB_DEBUG_MIN + '.js'
) }}"></script>
<script src="{{ url_for( 'static',
filename = 'split/split' + WEB_DEBUG_MIN + '.js'
) }}"></script>
<script src="{{ url_for( 'static',
filename = 'tinyemitter/tinyemitter' + WEB_DEBUG_MIN + '.js'
) }}"></script>
<script src="{{ url_for( 'static', filename='growl/jquery.growl.js' ) }}"></script>
<script>
// create the main application
gMainApp = Vue.createApp( {
template: "<main-app />",
} ) ;
gGetContentDocsUrl = "{{ url_for( 'get_content_docs') }}" ;
</script>
<script src="{{url_for('static',filename='src/MainApp.js')}}"></script>
<script> gMainApp.mount( "#main-app" ) ; </script>
<script type="module" src="{{ url_for( 'static', filename='MainApp.js' ) }}"></script>
<script type="module" src="{{ url_for( 'static', filename='NavPane.js' ) }}"></script>
<script type="module" src="{{ url_for( 'static', filename='SearchPane.js' ) }}"></script>
<script type="module" src="{{ url_for( 'static', filename='SearchResult.js' ) }}"></script>
<script type="module" src="{{ url_for( 'static', filename='ContentPane.js' ) }}"></script>
<script type="module" src="{{ url_for( 'static', filename='TabbedPages.js' ) }}"></script>
<script type="module" src="{{ url_for( 'static', filename='utils.js' ) }}"></script>
</html>

@ -5,6 +5,9 @@ from google.protobuf.empty_pb2 import Empty
from asl_rulebook2.webapp.tests.proto.generated.control_tests_pb2_grpc import ControlTestsStub
from asl_rulebook2.webapp.tests.proto.generated.control_tests_pb2 import \
SetDataDirRequest
# ---------------------------------------------------------------------
# NOTE: The API for this class should be kept in sync with ControlTestsServicer.
@ -25,3 +28,10 @@ class ControlTests:
def end_tests( self ):
"""End a test run."""
self._stub.endTests( Empty() )
def set_data_dir( self, fixtures_dname ):
"""Set the data directory."""
self._stub.setDataDir(
SetDataDirRequest( fixturesDirName = fixtures_dname )
)
return self

@ -1,5 +1,6 @@
""" gRPC servicer that allows the webapp server to be controlled. """
import os
import inspect
import logging
@ -7,6 +8,8 @@ from google.protobuf.empty_pb2 import Empty
from asl_rulebook2.webapp.tests.proto.generated.control_tests_pb2_grpc \
import ControlTestsServicer as BaseControlTestsServicer
from asl_rulebook2.webapp.tests.proto.generated.control_tests_pb2 import \
SetDataDirRequest
_logger = logging.getLogger( "control_tests" )
@ -20,6 +23,7 @@ class ControlTestsServicer( BaseControlTestsServicer ):
def __init__( self, webapp ):
# initialize
self._webapp = webapp
self._fixtures_dir = os.path.join( os.path.dirname(__file__), "fixtures/" )
def __del__( self ):
# clean up
@ -32,6 +36,11 @@ class ControlTestsServicer( BaseControlTestsServicer ):
def startTests( self, request, context ):
"""Start a new test run."""
self._log_request( request, context )
# reset the webapp
ctx = None
self.setDataDir( SetDataDirRequest( fixturesDirName=None ), ctx )
# NOTE: The webapp has now been reset, but the client must reloaed the home page
# with "?reload=1", to force it to reload with the new settings.
return Empty()
def endTests( self, request, context ):
@ -40,9 +49,24 @@ class ControlTestsServicer( BaseControlTestsServicer ):
self.cleanup()
return Empty()
def setDataDir( self, request, context ):
"""Set the data directory."""
self._log_request( request, context )
dname = request.fixturesDirName
# set the data directory
_logger.debug( "- Setting data directory: %s", dname )
if dname:
self._webapp.config[ "DATA_DIR" ] = os.path.join( self._fixtures_dir, dname )
_logger.warning( os.path.join( self._fixtures_dir, dname ) )
else:
self._webapp.config.pop( "DATA_DIR", None )
return Empty()
@staticmethod
def _log_request( req, ctx ): #pylint: disable=unused-argument
"""Log a request."""
if ctx is None:
return # nb: we don't log internal calls
# get the entry-point name
msg = "{}()".format( inspect.currentframe().f_back.f_code.co_name )
# log the message

@ -0,0 +1,76 @@
[
{ "title": "a",
"content": "amphibious"
},
{ "title": "Advance",
"ruleids": [ "A4.7" ]
},
{ "title": "Backblast",
"ruleids": [ "C13.8" ],
"rulerefs": [
{ "caption": "Huts", "ruleids": [ "G5.62" ] },
{ "caption": "RCL", "ruleids": [ "C12.3-.4" ] }
]
},
{ "title": "CCPh",
"subtitle": "Close Combat Phase",
"ruleids": [ "A3.8" ],
"rulerefs": [
{ "caption": "ENEMY Attacks", "ruleids": [ "S11.5" ] },
{ "caption": "dropping SW before CC", "ruleids": [ "A4.43" ] }
]
},
{ "title": "Double Time",
"ruleids": [ "A4.5-.51", "S6.222" ],
"see_also": [ "CX" ],
"content": "Also known as \"running <em>really</em> fast.\"",
"rulerefs": [
{ "caption": "ENEMY Guard Automatic Action", "ruleids": [ "S6.303" ] },
{ "caption": "Manhandling", "ruleids": [ "C10.3" ] },
{ "caption": "NA for Pathfinders", "ruleids": [ "T1.2" ] },
{ "caption": "S? NA", "ruleids": [ "S3.321" ] },
{ "caption": "Water Shortage", "ruleids": [ "RCG21" ] },
{ "caption": "Wire NA", "ruleids": [ "B26.46" ] }
]
},
{ "title": "ELR",
"subtitle": "Experience Level Rating",
"ruleids": [ "A19.1" ],
"rulerefs": [
{ "caption": "BRT", "ruleids": [ "TCG17" ] },
{ "caption": "Loss", "ruleids": [ "A16.2" ] },
{ "caption": "Massacre", "ruleids": [ "A20.4" ] },
{ "caption": "Night", "ruleids": [ "E1.22" ] },
{ "caption": "in PB", "ruleids": [ "SSR PB12" ] },
{ "caption": "Regaining", "ruleids": [ "A16.3" ] },
{ "caption": "RePh", "ruleids": [ "O11.617", "PCG4", "QCG3", "R9.6202" ] }
]
},
{ "title": "Firepower",
"ruleids": [ "A1.21" ],
"see_also": [ "FP" ]
},
{ "title": "Gaps, Convoy",
"ruleids": [ "E11.21" ]
},
{ "title": "H#",
"subtitle": "HEAT Depletion Number; the number is the Depletion Number, and the superscript following it indicates the first year it applies and a letter indicates the month of that year [EX: A superscript of \"4\" means the vehicle/ordnance has that ammo starting in 1944]",
"ruleids": [ "C8.3" ],
"see_also": [ "HEAT" ]
},
{ "title": "Identity, Vehicular",
"ruleids": [ "D1.4" ]
}
]

@ -0,0 +1,15 @@
{
"A4.7": { "caption": "ADVANCE PHASE", "page_no": 1, "pos": [72,702] },
"C13.8": { "caption": "BACKBLAST", "page_no": 1, "pos": [72,404] },
"A3.8": { "caption": "CLOSE COMBAT PHASE (CCPh)", "page_no": 1, "pos": [72.97] },
"A4.5": { "caption": "DOUBLE TIME", "page_no": 2, "pos": [72,702] },
"A19.1": { "caption": "EXPERIENCE LEVEL RATING (ELR)", "page_no": 2, "pos": [72.404] },
"A1.21": { "caption": "FIREPOWER (FP)", "page_no": 2, "pos": [72,97] },
"A1.21": { "caption": "FIREPOWER (FP)", "page_no": 3, "pos": [72,702] },
"E11.21": { "caption": "GAPS", "page_no": 3, "pos":[72,404] },
"C8.3": { "caption": "HEAT (H)", "page_no": 3, "pos": [72,97] }
}

@ -4,8 +4,16 @@ import "google/protobuf/empty.proto" ;
// --------------------------------------------------------------------
message SetDataDirRequest {
string fixturesDirName = 1 ;
}
// --------------------------------------------------------------------
service ControlTests
{
rpc startTests( google.protobuf.Empty ) returns ( google.protobuf.Empty ) ;
rpc endTests( google.protobuf.Empty ) returns ( google.protobuf.Empty ) ;
rpc setDataDir( SetDataDirRequest ) returns ( google.protobuf.Empty ) ;
}

@ -20,14 +20,54 @@ DESCRIPTOR = _descriptor.FileDescriptor(
syntax='proto3',
serialized_options=None,
create_key=_descriptor._internal_create_key,
serialized_pb=b'\n\x13\x63ontrol_tests.proto\x1a\x1bgoogle/protobuf/empty.proto2\x88\x01\n\x0c\x43ontrolTests\x12<\n\nstartTests\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12:\n\x08\x65ndTests\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Emptyb\x06proto3'
serialized_pb=b'\n\x13\x63ontrol_tests.proto\x1a\x1bgoogle/protobuf/empty.proto\",\n\x11SetDataDirRequest\x12\x17\n\x0f\x66ixturesDirName\x18\x01 \x01(\t2\xc2\x01\n\x0c\x43ontrolTests\x12<\n\nstartTests\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12:\n\x08\x65ndTests\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12\x38\n\nsetDataDir\x12\x12.SetDataDirRequest\x1a\x16.google.protobuf.Emptyb\x06proto3'
,
dependencies=[google_dot_protobuf_dot_empty__pb2.DESCRIPTOR,])
_SETDATADIRREQUEST = _descriptor.Descriptor(
name='SetDataDirRequest',
full_name='SetDataDirRequest',
filename=None,
file=DESCRIPTOR,
containing_type=None,
create_key=_descriptor._internal_create_key,
fields=[
_descriptor.FieldDescriptor(
name='fixturesDirName', full_name='SetDataDirRequest.fixturesDirName', index=0,
number=1, type=9, cpp_type=9, label=1,
has_default_value=False, default_value=b"".decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
],
extensions=[
],
nested_types=[],
enum_types=[
],
serialized_options=None,
is_extendable=False,
syntax='proto3',
extension_ranges=[],
oneofs=[
],
serialized_start=52,
serialized_end=96,
)
DESCRIPTOR.message_types_by_name['SetDataDirRequest'] = _SETDATADIRREQUEST
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
SetDataDirRequest = _reflection.GeneratedProtocolMessageType('SetDataDirRequest', (_message.Message,), {
'DESCRIPTOR' : _SETDATADIRREQUEST,
'__module__' : 'control_tests_pb2'
# @@protoc_insertion_point(class_scope:SetDataDirRequest)
})
_sym_db.RegisterMessage(SetDataDirRequest)
_CONTROLTESTS = _descriptor.ServiceDescriptor(
@ -37,8 +77,8 @@ _CONTROLTESTS = _descriptor.ServiceDescriptor(
index=0,
serialized_options=None,
create_key=_descriptor._internal_create_key,
serialized_start=53,
serialized_end=189,
serialized_start=99,
serialized_end=293,
methods=[
_descriptor.MethodDescriptor(
name='startTests',
@ -60,6 +100,16 @@ _CONTROLTESTS = _descriptor.ServiceDescriptor(
serialized_options=None,
create_key=_descriptor._internal_create_key,
),
_descriptor.MethodDescriptor(
name='setDataDir',
full_name='ControlTests.setDataDir',
index=2,
containing_service=None,
input_type=_SETDATADIRREQUEST,
output_type=google_dot_protobuf_dot_empty__pb2._EMPTY,
serialized_options=None,
create_key=_descriptor._internal_create_key,
),
])
_sym_db.RegisterServiceDescriptor(_CONTROLTESTS)

@ -2,6 +2,7 @@
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import asl_rulebook2.webapp.tests.proto.generated.control_tests_pb2 as control__tests__pb2
from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
@ -26,6 +27,11 @@ class ControlTestsStub(object):
request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString,
response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString,
)
self.setDataDir = channel.unary_unary(
'/ControlTests/setDataDir',
request_serializer=control__tests__pb2.SetDataDirRequest.SerializeToString,
response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString,
)
class ControlTestsServicer(object):
@ -45,6 +51,12 @@ class ControlTestsServicer(object):
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def setDataDir(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_ControlTestsServicer_to_server(servicer, server):
rpc_method_handlers = {
@ -58,6 +70,11 @@ def add_ControlTestsServicer_to_server(servicer, server):
request_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString,
response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString,
),
'setDataDir': grpc.unary_unary_rpc_method_handler(
servicer.setDataDir,
request_deserializer=control__tests__pb2.SetDataDirRequest.FromString,
response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'ControlTests', rpc_method_handlers)
@ -103,3 +120,20 @@ class ControlTests(object):
google_dot_protobuf_dot_empty__pb2.Empty.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def setDataDir(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/ControlTests/setDataDir',
control__tests__pb2.SetDataDirRequest.SerializeToString,
google_dot_protobuf_dot_empty__pb2.Empty.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)

@ -1,10 +1,20 @@
""" Test basic functionality. """
from asl_rulebook2.webapp.tests.utils import init_webapp
from asl_rulebook2.webapp.tests.utils import init_webapp, get_nav_panels, get_content_docs
# ---------------------------------------------------------------------
def test_hello( webapp, webdriver ):
"""Test basic functionality."""
# initialize
webapp.control_tests.set_data_dir( "simple" )
init_webapp( webapp, webdriver )
# check that the nav panel loaded correctly
nav_panels = get_nav_panels()
assert nav_panels == [ "search" ]
# check that the content docs loaded correctly
content_docs = get_content_docs()
assert content_docs == [ "simple" ]

@ -0,0 +1,35 @@
""" Run ESLint over the Javascript files. """
import os.path
import subprocess
import pytest
# ---------------------------------------------------------------------
@pytest.mark.skipif( not os.environ.get("ESLINT"), reason="ESLINT not configured." )
def test_eslint():
"""Run ESLint over the Javascript files."""
# initialize
eslint = os.environ[ "ESLINT" ]
# check each Javascript file
dname = os.path.join( os.path.dirname(__file__), "../static/" )
for fname in os.listdir( dname ):
if os.path.splitext( fname )[1] != ".js":
continue
# run ESLint for the next file
proc = subprocess.run(
[ eslint, os.path.join(dname,fname) ],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8",
check=False
)
if proc.stdout or proc.stderr:
print( "=== ESLint failed: {} ===".format( fname ) )
if proc.stdout:
print( proc.stdout )
if proc.stderr:
print( proc.stderr )
assert False

@ -3,6 +3,8 @@
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import NoSuchElementException
from asl_rulebook2.webapp import tests as webapp_tests
_webapp = None
_webdriver = None
@ -17,6 +19,11 @@ def init_webapp( webapp, webdriver, **options ):
_webdriver = webdriver
# load the webapp
if get_pytest_option("webdriver") == "chrome" and get_pytest_option("headless"):
# FUDGE! Headless Chrome doesn't want to show the PDF in the browser,
# it downloads the file and saves it in the current directory :wtf:
options["no-content"] = 1
options["reload"] = 1 # nb: force the webapp to reload
webdriver.get( webapp.url_for( "main", **options ) )
_wait_for_webapp()
@ -32,6 +39,21 @@ def _wait_for_webapp():
# ---------------------------------------------------------------------
def get_nav_panels():
"""Get the available nav panels."""
return _get_tab_ids( "#nav .tab-strip" )
def get_content_docs():
"""Get the available content docs."""
return _get_tab_ids( "#content .tab-strip" )
def _get_tab_ids( sel ):
"""Get the tabs in a tab-strip."""
tabs = find_children( "{} .tab".format( sel ) )
return [ tab.get_attribute( "data-tabid" ) for tab in tabs ]
# ---------------------------------------------------------------------
def find_child( sel, parent=None ):
"""Find a single child element."""
try:
@ -41,6 +63,15 @@ def find_child( sel, parent=None ):
except NoSuchElementException:
return None
def find_children( sel, parent=None ):
"""Find child elements."""
try:
if parent is None:
parent = _webdriver
return parent.find_elements_by_css_selector( sel )
except NoSuchElementException:
return None
# ---------------------------------------------------------------------
def wait_for( timeout, func ):
@ -48,3 +79,9 @@ def wait_for( timeout, func ):
WebDriverWait( _webdriver, timeout, poll_frequency=0.1 ).until(
lambda driver: func()
)
# ---------------------------------------------------------------------
def get_pytest_option( opt ):
"""Get a pytest configuration option."""
return getattr( webapp_tests.pytest_options, opt )

@ -1,7 +1,25 @@
"""Helper functions."""
import pathlib
import re
# ---------------------------------------------------------------------
def change_extn( fname, extn ):
"""Change a filename's extension."""
return pathlib.Path( fname ).with_suffix( extn )
def slugify( val ):
"""Convert a string to a slug."""
val = re.sub( r"\s+", " ", val ).lower()
def fix( ch ):
if ch.isalnum() or ch == "-":
return ch
if ch in " _":
return "-"
return "_"
return "".join( fix(ch) for ch in val )
def parse_int( val, default=None ):
"""Parse an integer."""
try:

Loading…
Cancel
Save