Allow single-line textbox's to be edited as HTML.

master
Pacman Ghost 2 years ago committed by Pacman Ghost
parent a09286bc50
commit 53e14f753f
  1. 2
      vasl_templates/webapp/data/default-template-pack/extras/grid.j2
  2. 6
      vasl_templates/webapp/main.py
  3. 11
      vasl_templates/webapp/static/css/desktop.css
  4. 4
      vasl_templates/webapp/static/css/edit-html-textbox-dialog.css
  5. 15
      vasl_templates/webapp/static/css/edit-vo-dialog.css
  6. 22
      vasl_templates/webapp/static/css/html-editor.css
  7. 2
      vasl_templates/webapp/static/css/main.css
  8. 1
      vasl_templates/webapp/static/css/tabs-extras.css
  9. 21
      vasl_templates/webapp/static/css/tabs-scenario.css
  10. 26
      vasl_templates/webapp/static/extras.js
  11. 203
      vasl_templates/webapp/static/html-editor.js
  12. 41
      vasl_templates/webapp/static/main.js
  13. 4
      vasl_templates/webapp/static/scenario-upload.js
  14. 17
      vasl_templates/webapp/static/scenarios.js
  15. 9
      vasl_templates/webapp/static/simple_notes.js
  16. 44
      vasl_templates/webapp/static/snippets.js
  17. 3
      vasl_templates/webapp/static/utils.js
  18. 9
      vasl_templates/webapp/static/vassal.js
  19. 42
      vasl_templates/webapp/static/vo2.js
  20. 5
      vasl_templates/webapp/templates/edit-html-textbox-dialog.html
  21. 2
      vasl_templates/webapp/templates/index.html
  22. 12
      vasl_templates/webapp/templates/tabs-scenario.html
  23. 7
      vasl_templates/webapp/tests/fixtures/new-default-scenario.json
  24. 71
      vasl_templates/webapp/tests/test_capabilities.py
  25. 15
      vasl_templates/webapp/tests/test_default_scenario.py
  26. 45
      vasl_templates/webapp/tests/test_dirty_scenario_checks.py
  27. 58
      vasl_templates/webapp/tests/test_html.py
  28. 11
      vasl_templates/webapp/tests/test_scenario_persistence.py
  29. 9
      vasl_templates/webapp/tests/test_scenario_search.py
  30. 5
      vasl_templates/webapp/tests/test_snippets.py
  31. 19
      vasl_templates/webapp/tests/utils.py

@ -3,7 +3,7 @@
<!-- vasl-templates:name Grid -->
<!-- vasl-templates:description Generates a grid. -->
<!-- caption = {{CAPTION:/20|Caption}} -->
<!-- caption = {{CAPTION*:/25|Caption}} -->
<!-- #cols = {{COLS:3/1|# columns}} ; #rows = {{ROWS:2/1|# rows}} -->
<!-- cell size = {{CELL_WIDTH:180px/5|Cell width}} x {{CELL_HEIGHT:70px/5|Cell height}} -->
<!-- cell labels = {{CELL_LABELS:none::letters::numbers|Cell labels}} -->

@ -209,6 +209,12 @@ def get_app_config():
[ "specialChars", "flags", "emoji" ],
[ "removeformat", "historyUndo", "historyRedo", "viewHTML", "fullscreen" ],
] ),
"html-textbox-dialog": get_json_val( "TRUMBOWYG_BUTTONS_HTML_TEXTBOX_DIALOG", [
[ "strong", "em", "underline", "del", "superscript", "subscript" ],
[ "foreColor", "backColor" ],
[ "specialChars", "flags", "emoji" ],
[ "removeformat", "historyUndo", "historyRedo", "viewHTML", "fullscreen" ],
] ),
}
return jsonify( vals )

@ -0,0 +1,11 @@
/* NOTE: This file contains work-arounds for the desktop app. */
fieldset[name='scenario'] { max-height: 251px ; }
/* these work around vertical alignment of text in input controls */
#panel-scenario label { margin-bottom: -1px ; }
#panel-scenario label[for="TURN_TRACK_NTURNS"], #panel-scenario label[for="PLAYER_1"], #panel-scenario label[for="PLAYER_2"] {
line-height: 20px ;
}
#panel-scenario input[name="SCENARIO_DATE"] { padding-top: 2px ; }

@ -0,0 +1,4 @@
#edit-html_textbox-dialog { overflow: hidden ; padding-bottom: 0.75em !important ; }
.ui-dialog.edit-html_textbox .ui-dialog-buttonpane { border: none ; margin-top: 0 !important ; padding-top: 0 !important ; }
.ui-dialog.edit-html_textbox .container { height: 100% ; display: flex ; flex-direction: row ; }

@ -12,15 +12,22 @@
.ui-dialog.edit-vo .capabilities { margin-top: 0.25em ; display: flex ; flex-direction: column ; }
.ui-dialog.edit-vo .comments { margin-top: 0.5em ; display: flex ; flex-direction: column ; }
.ui-dialog.edit-vo .brewup { color: #a04010 ; }
.ui-dialog.edit-vo .split-mg-red { color: #a04010 ; }
.ui-dialog.edit-vo #vo_capabilities-sortable { overflow-x: hidden ; }
.ui-dialog.edit-vo #vo_comments-sortable { overflow-x: hidden ; }
.ui-dialog.edit-vo .fieldset { flex-grow: 1 ; display: flex ; flex-direction: column ; align-items: stretch ; }
.ui-dialog.edit-vo .fieldset { margin: 0 ; padding: 5px 8px 8px 8px ; border: 1px solid #aaa ; }
.ui-dialog.edit-vo .fieldset-legend { color: #333 ; font-weight: bold ; }
.ui-dialog.edit-vo .fieldset ul { flex-grow: 1 ; margin: 0 0 5px 0 ; list-style-type: none ; overflow-y: auto ; }
.ui-dialog.edit-vo .fieldset li { margin: 2px ; padding: 2px 0px 2px 5px ; background: #eee ; }
.ui-dialog.edit-vo .fieldset li div { display: flex ; align-items: center ; }
.ui-dialog.edit-vo .fieldset li img.dragger { height: 1em ; margin-right: 5px ; }
.ui-dialog.edit-vo .fieldset li input[type='text'] { flex-grow: 1 ; margin-right: 1em ; }
.ui-dialog.edit-vo .fieldset li { margin: 2px ; padding: 2px 0px 2px 5px ; background: #eee ; max-width: 100% ; }
.ui-dialog.edit-vo .fieldset li { height: 22px ; border: none !important ; }
.ui-dialog.edit-vo .fieldset li:hover { cursor: auto ; }
.ui-dialog.edit-vo .fieldset li > div { display: flex ; align-items: center ; padding-right: 2px ; }
.ui-dialog.edit-vo .fieldset li img.dragger { height: 1em ; margin-right: 5px ; cursor: pointer ; }
.ui-dialog.edit-vo .fieldset li div.html-textbox { flex-grow: 1 ; }
.ui-dialog.edit-vo .fieldset .footer { margin-top: 0.25em ; display: flex ; align-items: center ; }
.ui-dialog.edit-vo .fieldset .footer img.trash { margin: 3px 5px ; height: 24px ; }

@ -62,3 +62,25 @@
.Xtrumbowyg-specialChars-button { color: #555 ; }
.trumbowyg-indent-button svg, .trumbowyg-outdent-button svg { width: 13px ; }
.trumbowyg-indent-button svg { transform: scaleX(-1) ; }
/* -------------------------------------------------------------------- */
div.html-textbox {
flex-grow: 100 ;
height: 20px ; max-height: 20px ;
/* NOTE: We hide vertical overflow when the content goes multi-line, and the h-scrollbar if very long words
* (i.e. without spaces) are present. In the latter case, the layout breaks if the control is in a flexbox,
* since it just expands out, regardless of any max-width setting :-/, so we work-around this by dynamically
* setting a max-width on the parent row.
* */
overflow: hidden ;
line-height: 1.4em ; /* FUDGE! This works around a vertical-alignment problem in the desktop app. */
padding: 0 12px 0 5px ;
border: 1px solid #c5c5c5 ; background: white ;
/* NOTE: This makes the control scroll horizontally (like a real textbox), but it breaks padding (which is maybe
* acceptable), and max-width (which is not). Things don't work well when these things are in a flexbox, and while
* we can sorta get things working by disabling that, and dynamically setting the width along with max-width,
* weird things happen when we resize the enclosing panel :-/
white-space: nowrap ; overflow-x: hidden ;
*/
}

@ -7,7 +7,7 @@ body { font-family: Arial, Helvetica, sans-serif ; font-size: 16px ; }
p { margin-bottom: 0.5em ; }
ul, ol { margin: 0.5em 0 0 1.25em ; }
input[type="text"] { height: 20px ; border: 1px solid #c5c5c5 ; padding: 0 2px ; }
input[type="text"] { height: 20px ; border: 1px solid #c5c5c5 ; padding: 0 5px ; }
label { height: 1.25em ; }
/* -------------------------------------------------------------------- */

@ -22,6 +22,7 @@
#tabs-extras .right-panel table { margin-top: 1em ; }
#tabs-extras .right-panel td.caption { font-weight: bold ; padding-right: 0.25em ; }
#tabs-extras .right-panel td.value { display: flex ; align-items: center ; }
#tabs-extras .right-panel .snippet-control { margin-top: 0.5em ; }
#tabs-extras .right-panel .footer { margin-top: 1em ; font-size: 80% ; font-style: italic ; color: #444 ; }

@ -1,26 +1,33 @@
/* -------------------------------------------------------------------- */
#panel-scenario { display: flex ; flex-direction: column ; }
fieldset[name='scenario'] { min-height: 91px ; max-height: 265px ; overflow: hidden ; }
fieldset[name='scenario'] {
min-height: 91px ; overflow: hidden ;
max-height: 255px ; /* nb: overridden in desktop.css */
}
#panel-scenario .row { display: flex ; align-items: center ; margin-bottom: 2px ; }
#panel-scenario input { flex-grow: 1 ; }
#panel-scenario input[name='SCENARIO_ID'] { margin-left: 0.25em ; width: 70px ; flex-grow: 0 ; text-align: right ; }
#panel-scenario div.html-textbox[name='SCENARIO_ID'] { margin-left: 0.25em ; width: 70px ; flex-grow: 0 ; }
#panel-scenario .scenario-search { width: 25px ; height: 22px ; margin: 0 0 0 0.25em ; padding: 0 ; }
#panel-scenario .scenario-search img { margin-top: 2px ; width: 16px ; }
#panel-scenario input[name='SCENARIO_DATE'] { width: 6em ; flex-grow: 0 ; }
#panel-scenario input[name='SCENARIO_DATE'] {
/* nb: override in desktop.css for v-alignment */
width: 6em ; flex-grow: 0 ; color: #333 ;
}
#panel-scenario button#turn-track-settings { width: 25px ; height: 24px ; margin-right: 5px ; padding: 3px 2px 2px 2px ; }
#panel-scenario .select2[name="PLAYER_1"] { flex: 1 ; }
#panel-scenario .select2[name="PLAYER_2"] { flex: 1 ; }
#panel-scenario input[name="PLAYER_1_DESCRIPTION"],
#panel-scenario input[name="PLAYER_2_DESCRIPTION"] {
#panel-scenario div.html-textbox[name="PLAYER_1_DESCRIPTION"],
#panel-scenario div.html-textbox[name="PLAYER_2_DESCRIPTION"] {
font-size: 75% ;
height: 14px ; max-height: 14px ; line-height: 14px ;
}
#panel-scenario label { margin-top: 2px ; font-weight: bold ; width: 4.75em ; }
/* nb: see desktop.css for overrides to this panel's labels */
#panel-scenario label { height: 18px ; font-weight: bold ; width: 4.75em ; }
#panel-scenario label.header { font-weight: bold ; width: 3em ; text-align: center ; }
#panel-scenario .select2-container { margin-right: 2px ; }

@ -95,6 +95,16 @@ function _show_extra_template( template_id )
if ( template_info.params[i].description )
buf.push( " title='" + escapeHTML(template_info.params[i].description) + "'" ) ;
buf.push( ">" ) ;
} else if ( template_info.params[i].type == "html_textbox" ) {
buf.push( "<div class='param html-textbox' name='" + escapeHTML(template_info.params[i].name) + "'" ) ;
if ( template_info.params[i].width )
buf.push( " style='width:" + (template_info.params[i].width * 0.75*gEmSize) + "px;'" ) ;
if ( template_info.params[i].description )
buf.push( " title='" + escapeHTML(template_info.params[i].description) + "'" ) ;
buf.push( ">" ) ;
if ( template_info.params[i].default )
buf.push( sanitizeHTML( template_info.params[i].default ) ) ;
buf.push( "</div>" ) ;
} else if ( template_info.params[i].type === "select" ) {
buf.push( "<select class='param' name='" + escapeHTML(template_info.params[i].name) + "' style='width:6em;'>" ) ;
for ( j=0 ; j < template_info.params[i].options.length ; ++j )
@ -164,6 +174,12 @@ function _show_extra_template( template_id )
} ).on( "select2:open", function() {
restrict_droplist_height( $(this) ) ;
} ) ;
$form.find( "div.html-textbox" ).each( function() {
var title = $(this).attr( "title" ) ;
if ( title )
title = title[0].toLowerCase() + title.substring(1) ;
initHtmlTextbox( $(this), title, false ) ;
} ) ;
fixup_external_links( $form ) ;
var $sel = $form.find( "select[name='_PLAYER_DROPLIST_']" ) ;
if ( $sel.length > 0 ) {
@ -219,8 +235,12 @@ function _parse_extra_template( template_id, template )
else if ( param.name === "PLAYER_COLOR_DROPLIST" )
param.type = "player-color-droplist" ;
else {
// we have an <input>
param.type = "input" ;
// we have an <input> or HTML textbox
if ( param.name.substring( param.name.length-1 ) === "*" ) {
param.type = "html_textbox" ;
param.name = param.name.substring( 0, param.name.length-1 ) ;
} else
param.type = "input" ;
// extract the default value and field width
pos = val.indexOf( "/" ) ;
if ( pos === -1 )
@ -246,7 +266,7 @@ function _parse_extra_template( template_id, template )
function fixup_template_parameters( template )
{
// identify any non-standard template parameters
var regex = /\{\{([A-Z0-9_]+?):.*?\}\}/g ;
var regex = /\{\{([A-Z0-9_]+?)\*?:.*?\}\}/g ;
var matches = [] ;
var match ;
while( (match = regex.exec( template )) !== null )

@ -1,6 +1,8 @@
gEditHtmlTextboxDlgState = null ;
// --------------------------------------------------------------------
function initTrumbowyg( $elem, buttons, $parentDlg )
function initTrumbowyg( $ctrl, buttons, $parentDlg )
{
// initialize
var nats = get_sorted_nats().filter(
@ -14,7 +16,7 @@ function initTrumbowyg( $elem, buttons, $parentDlg )
// from the WYSIWYG control to the raw HTML textarea, it doesn't really help, since manipulating
// the content in the <textarea> directly doesn't work, we need to use Trumbowyg's "html" API, and that
// works from the WYSIWYG control.
$elem.trumbowyg( {
$ctrl.trumbowyg( {
btnsDef: {
format: {
dropdown: gAppConfig.trumbowyg[ "format-options" ],
@ -55,19 +57,19 @@ function initTrumbowyg( $elem, buttons, $parentDlg )
},
tagsToRemove: gAppConfig.trumbowyg[ "tag-blacklist" ],
} ) ;
var $parent = $elem.parent() ;
var $parent = $ctrl.parent() ;
var $btnPane = $parent.find( ".trumbowyg-button-pane" ) ;
var $textarea = $parent.find( ".trumbowyg-textarea" ) ;
// update the flags dropdown for the current players
if ( $btnPane.find( ".trumbowyg-flags-button" ).length > 0 )
updateTrumbowygFlagsDropdown( $elem ) ;
updateTrumbowygFlagsDropdown( $ctrl ) ;
// prepare for our jQuery event handlers
var eventHandlers = $elem.data( "eventHandlers" ) ;
var eventHandlers = $ctrl.data( "eventHandlers" ) ;
if ( ! eventHandlers ) {
eventHandlers = new jQueryHandlers() ;
$elem.data( "eventHandlers", eventHandlers ) ;
$ctrl.data( "eventHandlers", eventHandlers ) ;
}
// allow a hotkey to toggle the WYSIWYG editor
@ -78,12 +80,12 @@ function initTrumbowyg( $elem, buttons, $parentDlg )
function onKeyDown( evt ) {
// check for Ctrl-M
if ( evt.keyCode == 77 && evt.ctrlKey ) {
$elem.trumbowyg( "toggle" ) ;
$ctrl.trumbowyg( "toggle" ) ;
setTimeout( function() {
if ( $elem.parent().hasClass( "trumbowyg-editor-visible" ) )
$elem.focus() ;
if ( $ctrl.parent().hasClass( "trumbowyg-editor-visible" ) )
$ctrl.focus() ;
else
$elem.parent().find( ".trumbowyg-textarea" ).focus() ;
$ctrl.parent().find( ".trumbowyg-textarea" ).focus() ;
}, 20 ) ;
evt.preventDefault() ;
return ;
@ -92,7 +94,7 @@ function initTrumbowyg( $elem, buttons, $parentDlg )
if ( $parentDlg )
auto_dismiss_dialog( $parentDlg, evt, "OK" ) ;
}
eventHandlers.addHandler( $elem, "keydown", onKeyDown ) ;
eventHandlers.addHandler( $ctrl, "keydown", onKeyDown ) ;
eventHandlers.addHandler( $textarea, "keydown", onKeyDown ) ;
// FUDGE! There should be spaces around the +, but this causes the tooltip to wrap on Windows :-/
$btnPane.find( ".trumbowyg-viewHTML-button" ).attr( "title", "View HTML (Ctrl+M)" ) ;
@ -102,14 +104,23 @@ function initTrumbowyg( $elem, buttons, $parentDlg )
var resizeObserver = new ResizeObserver( function( entries ) {
// FUDGE! Couldn't get Trumbowyg to sit nicely inside a flexbox, so we set the height dynamically :-/
var height = "calc(100% - " + $btnPane.height() + "px)" ;
$elem.css( { height: height } ) ;
$ctrl.css( { height: height } ) ;
$textarea.css( { height: height } ) ;
// limit the height of dropdown's
if ( $parentDlg ) {
$parent.find( ".trumbowyg-dropdown" ).css( {
"max-height": $elem.height() + 5
"max-height": $ctrl.height() + 5
} ) ;
}
// FUDGE! We also need to stop the HTML textboxes that are in a flexbox from expanding out
// if they contain long words with no spaces. The layout still isn't quite right, but this
// isn't something that will happen often, so we just live with it :-/
// NOTE: Things work when the SCENARIO panel gets wider, but not when it narrows (because
// the HTML textbox has expanded out, and doesn't want to narrow when the parent element
// narrows, and so the panel doesn't narrow). We work-around this by checking the width
// of the SCENARIO NOTES panel, which will always be the same width as the SCENARIO panel.
var $panel = $( "fieldset[name='scenario_notes']" ) ;
$( ".row" ).css( "max-width", $panel.width() ) ;
} ) ;
resizeObserver.observe( $parent[0] ) ;
$parent.data( "resizeObserver", resizeObserver ) ;
@ -118,23 +129,32 @@ function initTrumbowyg( $elem, buttons, $parentDlg )
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function destroyTrumbowyg( $elem )
function destroyTrumbowyg( $ctrl )
{
// destroy the Trumbowyg control and clean up
var eventHandlers = $elem.data( "eventHandlers" ) ;
var eventHandlers = $ctrl.data( "eventHandlers" ) ;
if ( eventHandlers ) {
eventHandlers.cleanUp() ;
$elem.removeData( "eventHandlers" ) ;
$ctrl.removeData( "eventHandlers" ) ;
}
$elem.trumbowyg( "destroy" ) ;
$ctrl.trumbowyg( "destroy" ) ;
}
// --------------------------------------------------------------------
function unloadTrumbowyg( $elem, removeFirstPara )
function resetTrumbowyg( $ctrl )
{
// reset the Trumbowyg control
if ( $ctrl.parent().hasClass( "trumbowyg-fullscreen" ) )
$ctrl.trumbowyg( "execCmd", { cmd: "fullscreen" } ) ;
if ( $ctrl.parent().hasClass( "trumbowyg-editor-hidden" ) )
$ctrl.trumbowyg( "toggle" ) ;
}
function unloadTrumbowyg( $ctrl, removeFirstPara )
{
// unload the Trumbowyg control
var val = $elem.trumbowyg( "html" ).trim() ;
var val = $ctrl.trumbowyg( "html" ).trim() ;
// FUDGE! Trumbowyg really wants to wrap everything in <p> blocks, but this causes problems
// since many of the templates are expecting a bit of plain old text, not blocks of content
@ -163,20 +183,20 @@ function unloadTrumbowyg( $elem, removeFirstPara )
function initVictoryConditionsTrumbowyg()
{
// initialize the Victory Conditions Trumbowyg control
var $elem = $( "div.param[name='VICTORY_CONDITIONS']" ) ;
initTrumbowyg( $elem, gAppConfig.trumbowyg["victory-conditions"], null ) ;
var $ctrl = $( "div.param[name='VICTORY_CONDITIONS']" ) ;
initTrumbowyg( $ctrl, gAppConfig.trumbowyg["victory-conditions"], null ) ;
if ( gPendingVictoryConditions )
$elem.trumbowyg( "html", gPendingVictoryConditions ) ;
$ctrl.trumbowyg( "html", gPendingVictoryConditions ) ;
// FUDGE! For some reason, we need to do this :shrug:
$elem.trumbowyg().on( "tbwopenfullscreen", function() {
$ctrl.trumbowyg().on( "tbwopenfullscreen", function() {
$( "#menu" ).hide() ;
} ).on( "tbwclosefullscreen", function() {
$( "#menu" ).show() ;
} ) ;
}
function updateTrumbowygFlagsDropdown( $elem )
function updateTrumbowygFlagsDropdown( $ctrl )
{
// FUDGE! For convenience, we show the flags for the current players at the start of the dropdown list,
// and while we can do this in makeDropdown() in our plugin, this only happens once for the Victory Conditions
@ -184,7 +204,7 @@ function updateTrumbowygFlagsDropdown( $elem )
// so we do it by modifying the DOM.
// initialize
var trumbowyg = $elem.data( "trumbowyg" ) ;
var trumbowyg = $ctrl.data( "trumbowyg" ) ;
if ( ! trumbowyg )
return ;
var plugin = trumbowyg.o.plugins.flags ;
@ -192,7 +212,7 @@ function updateTrumbowygFlagsDropdown( $elem )
var nat2 = get_player_nat( 2 ) ;
// locate the flags dropdown
$dropdown = $elem.parent().find( ".trumbowyg-dropdown-flags" ) ;
$dropdown = $ctrl.parent().find( ".trumbowyg-dropdown-flags" ) ;
if ( $dropdown.length === 0 )
return ;
@ -221,6 +241,122 @@ function updateTrumbowygFlagsDropdown( $elem )
// --------------------------------------------------------------------
var gNextHtmlTextboxId = 1 ;
function initHtmlTextbox( $ctrl, objName, small )
{
// NOTE: It's tricky designing a UX that allows single-line textbox's to be editable as HTML.
// Most of the time, fields such as scenario name and ID will be plain-text, so we present
// a contenteditable div (so that the user can make simple edits directly), with a discreet option
// to open a full WYSIWYG editor (in a dialog), if they want more complex content.
function onActivate( evt ) {
// show the "edit HTML" dialog
onEditHtmlTextbox( $ctrl, objName ) ;
evt.preventDefault() ;
}
// make the HTML textbox editable
var htbId = gNextHtmlTextboxId ++ ;
$ctrl.attr( {
contenteditable: true,
"data-htb-id": htbId,
} ) ;
$ctrl.click( function( evt ) {
// check for Alt-Click (to open the edit dialog)
// NOTE: We can't use Ctrl-Click, since that it used to delete caps/comments in the "edit v/o" dialog.
// NOTE: Alt-Click doesn't trigger an even on Linux, but Ctrl-Alt-Click and Shift-Alt-Click work... :-/
if ( evt.altKey )
onActivate( evt ) ;
} ).keydown( function( evt ) {
if ( evt.keyCode == 77 && evt.ctrlKey ) {
onActivate( evt ) ; // nb: Ctrl-M opens the "edit HTML" dialog
evt.preventDefault() ;
} else if ( evt.keyCode == $.ui.keyCode.ENTER )
evt.preventDefault() ; // nb: disable ENTER
} ) ;
// add an icon to open the "edit html textbox" dialog
var paramName = $ctrl.attr( "name" ) ;
var $img = $( "<svg class='edit-html-textbox' data-htb-id='" + htbId + "'>" +
"<use xlink:href='#trumbowyg-view-html'></use>" +
"<title> Edit HTML (Ctrl-M) </title>" +
"</svg>"
).css( {
width: "10px", height: "15px",
position: "relative", top: small?"-2px":"-4px", right: "13px",
"margin-right": "-10px",
opacity: 0.5,
cursor: "pointer",
} ) ;
$ctrl.after( $img ) ;
$img.click( function( evt ) {
onActivate( evt ) ;
} ) ;
}
function onEditHtmlTextbox( $ctrl, objName ) {
// initialize
var paramName = $ctrl.attr( "name" ) ; // nb: might be undefined e.g. vehicle/ordnance capabilities/comments
var dlgTitle = "Edit " + (objName || "HTML") ;
var $content, origVal ;
function unloadData() {
// unload the HTML content
return unloadTrumbowyg( $content, true ).trim() ;
}
// show the dialog
var $dlg = $( "#edit-html_textbox-dialog" ).dialog( {
dialogClass: "edit-html_textbox",
title: dlgTitle,
modal: true,
closeOnEscape: false,
position: gEditHtmlTextboxDlgState ? gEditHtmlTextboxDlgState.position : { my: "center", at: "center", of: window },
width: gEditHtmlTextboxDlgState ? gEditHtmlTextboxDlgState.width : $(window).width() * 0.5,
height: gEditHtmlTextboxDlgState ? gEditHtmlTextboxDlgState.height : Math.max( $(window).height() * 0.5, 325 ),
minWidth: 680, minHeight: 280,
create: function() {
init_dialog( $(this), "OK", true ) ;
},
open: function() {
$content = $(this).find( "div.content" ) ;
on_dialog_open( $(this), $content ) ;
// initialize the Trumbowyg HTML editor
if ( ! gEditHtmlTextboxDlgState ) // nb: check if this is the first time the dialog has been opened
initTrumbowyg( $content, gAppConfig.trumbowyg["html-textbox-dialog"], $(this) ) ;
else {
// always start non-maximized, and in HTML mode
resetTrumbowyg( $content ) ;
}
// load the dialog
$content.trumbowyg( "html", $ctrl.html().trim() ) ;
origVal = unloadData() ;
},
beforeClose: function() {
gEditHtmlTextboxDlgState = getDialogState( $(this) ) ;
},
buttons: {
OK: function() {
$ctrl.html( unloadData() ) ;
$(this).dialog( "close" ) ;
},
Cancel: function() {
if ( unloadData() != origVal ) {
ask( dlgTitle, "Discard your changes?", {
ok: function() { $dlg.dialog( "close" ) ; },
} ) ;
return ;
}
$(this).dialog( "close" ) ;
},
},
} ) ;
}
// --------------------------------------------------------------------
function sanitizeParams( params )
{
// recursively sanitize the scenario params
@ -230,11 +366,16 @@ function sanitizeParams( params )
if ( typeof params[key] === "object" )
sanitizeParams( params[key] ) ;
else if ( typeof params[key] === "string" ) {
var sanitized = DOMPurify.sanitize(
params[key],
{ USE_PROFILES: { html: true } }
) ;
params[key] = sanitized ;
params[key] = sanitizeHTML( params[key] ) ;
}
}
}
function sanitizeHTML( val )
{
// sanitize the HTML value
return DOMPurify.sanitize(
val,
{ USE_PROFILES: { html: true } }
) ;
}

@ -254,6 +254,24 @@ $(document).ready( function () {
// initialize the splitters
initSplitters() ;
// FUDGE! We show some fields in contenteditable div's, which want to auto-grow with their content.
// This doesn't play well when they're in a flexbox, so we set overflow-y to "hidden" to stop this
// from happening vertically, but have to dynamically set max-width to stop it horizontally :-/
var resizeObserver = new ResizeObserver( function( entries ) {
$( "div.html-textbox[name='SCENARIO_NAME']" ).css( {
"max-width": "calc(100% - 210px)"
} ) ;
$( "div.html-textbox[name='SCENARIO_LOCATION']" ).css( {
"max-width": "calc(100% - 237px)"
} ) ;
for ( var playerNo=1 ; playerNo <= 2 ; ++playerNo ) {
$( "div.html-textbox[name='PLAYER_" + playerNo + "_DESCRIPTION']" ).css( {
"max-width": "calc(100% - 7em - 8px)"
} ) ;
}
} ) ;
resizeObserver.observe( $( "#tabs-scenario .tl" )[0] ) ;
// get the application config
$.getJSON( gAppConfigUrl, function(data) {
gAppConfig = data ;
@ -426,8 +444,6 @@ $(document).ready( function () {
.button( {} ) ;
// watch for changes to the scenario details
$("input[name='SCENARIO_NAME']").on( "input propertychange paste", update_scenario_status ) ;
$("input[name='SCENARIO_ID']").on( "input propertychange paste", update_scenario_status ) ;
// NOTE: The following is to add/remove the "scenario modified" indicator. It's pretty inefficent
// to do this using a timer, but we would otherwise have to attach a "on change" event handler
// to every single input field, simple note, etc., which would be far more complicated and error-prone.
@ -506,7 +522,6 @@ $(document).ready( function () {
// flag that we've finished initialization
update_page_load_status( "main" ) ;
$("input[name='SCENARIO_NAME']").focus().focus() ;
} ) ;
function init_player_droplists()
@ -763,6 +778,12 @@ function update_page_load_status( id )
// check if the page has finished loading
if ( gPageLoadStatus.length === 0 ) {
// yup - update the UI
if ( gWebChannelHandler ) {
// inject CSS to work-around layout problems in the desktop app
$( "head" ).append(
"<link href='" + make_app_url("/static/css/desktop.css") + "' type='text/css' rel='stylesheet'>"
) ;
}
apply_user_settings() ;
$( "a[href='#tabs-extras']" ).html(
"<img src='" + gImagesBaseUrl + "/extras.png'>Extras"
@ -772,6 +793,14 @@ function update_page_load_status( id )
// initialize the HTML WYSIWYG editors (nb: we do it here, since we need the app config
// and template pack (for the player flags))
initVictoryConditionsTrumbowyg() ;
[ "SCENARIO_NAME", "SCENARIO_ID", "SCENARIO_LOCATION", "PLAYER_1_DESCRIPTION", "PLAYER_2_DESCRIPTION" ].forEach( function( key ) {
var $elem = $( "div.html-textbox[name='" + key + "']" ) ;
var caption = $elem.attr( "title" ) ;
initHtmlTextbox( $elem,
caption[0].toLowerCase() + caption.substring(1),
key.substr( 0, 7 ) === "PLAYER_"
) ;
} ) ;
// FUDGE! There are problems with the layout jumping around during startup in the desktop app,
// so we hide the footers on the scenario tab (which is the one visible during startup),
// and only show them them when we're ready.
@ -829,7 +858,7 @@ function init_hotkeys()
$ctrl.focus() ;
}
$(document).bind( "keydown", "alt+c", function() {
set_focus_to( "#tabs-scenario", $("input[name='SCENARIO_NAME']") ) ;
set_focus_to( "#tabs-scenario", $("div.html-textbox[name='SCENARIO_NAME']") ) ;
} ) ;
$(document).bind( "keydown", "alt+p", function() {
set_focus_to( "#tabs-scenario", $("select[name='PLAYER_1']") ) ;
@ -842,7 +871,7 @@ function init_hotkeys()
set_focus_to( "#tabs-scenario", $elem.parent().find( ".trumbowyg-textarea" ) ) ;
} ) ;
$(document).bind( "keydown", "alt+0", function() {
set_focus_to( "#tabs-scenario", $("input[name='SCENARIO_NAME']") ) ; // nb: for consistency with Alt-1 and Alt-2
set_focus_to( "#tabs-scenario", $("div.html-textbox[name='SCENARIO_NAME']") ) ; // nb: for consistency with Alt-1 and Alt-2
} ) ;
$(document).bind( "keydown", "alt+1", function() {
set_focus_to( "#tabs-ob1" ) ;
@ -959,7 +988,7 @@ function on_player_change( player_no )
// update the UI
var player_nat = update_ob_tab_header( player_no ) ;
update_nationality_specific_buttons( player_no ) ;
$( "input[name='PLAYER_" + player_no + "_DESCRIPTION']" ).val( "" ) ;
$( "div.html-textbox[name='PLAYER_" + player_no + "_DESCRIPTION']" ).html( "" ) ;
updateTrumbowygFlagsDropdown( $( ".param[name='VICTORY_CONDITIONS']" ) ) ;
// show/hide the vehicle/ordnance multi-applicable notes controls

@ -192,10 +192,10 @@ window.uploadScenario = function() {
// initialize
gVsavData = gScreenshotData = null ;
// load the dialog
var scenarioName = $("input[name='SCENARIO_NAME']").val().trim() ;
var scenarioName = $("div.html-textbox[name='SCENARIO_NAME']").text().trim() ;
if ( scenarioName )
$gDialog.find( ".scenario-name" ).text( scenarioName ) ;
var scenarioId = $("input[name='SCENARIO_ID']").val().trim() ;
var scenarioId = $("div.html-textbox[name='SCENARIO_ID']").text().trim() ;
if ( scenarioId )
$gDialog.find( ".scenario-id" ).text( "(" + scenarioId + ")" ) ;
$gDialog.find( ".asa-id" ).text( "(#" + asaScenarioId + ")" ) ;

@ -703,7 +703,7 @@ function onImportScenario()
function doImportScenario( scenario )
{
// reset the turn track
// reset the scenario settings
$( "#panel-scenario input" ).each( function() {
if ( $(this).attr( "name" ).substr( 0, 11 ) !== "TURN_TRACK_" )
return ;
@ -713,6 +713,9 @@ function doImportScenario( scenario )
else if ( inputType === "checkbox" )
$(this).prop( "checked", false ) ;
} ) ;
$( "#panel-scenario div.html-editor" ).each( function() {
$(this).html( "" ) ;
} ) ;
// reset the compass
$( "#panel-scenario input[name='COMPASS']" ).val( "" ) ;
@ -782,12 +785,20 @@ function checkImportField_text( importField, newVal, scenario ) {
function getImportFieldCurrVal_text( importField ) {
// get the current field value
return $( "input[name='" + importField.paramName + "']" ).val().trim() ;
var $elem = $( ".param[name='" + importField.paramName + "']" ) ;
if ( $elem.hasClass( "html-textbox" ) )
return $elem.html().trim() ;
else
return $elem.val().trim() ;
}
function doImportField_text( importField, newVal, scenario ) {
// update the field in the scenario
$( "input[name='" + importField.paramName + "']" ).val( newVal ) ;
var $elem = $( ".param[name='" + importField.paramName + "']" ) ;
if ( $elem.hasClass( "html-textbox" ) )
$elem.html( newVal ) ;
else
$elem.val( newVal ) ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

@ -111,7 +111,7 @@ function _do_edit_simple_note( template_id, player_no, $sortable2, $entry, defau
if ( $width.length === 0 ) {
// create the width controls
$btn_pane.prepend( $( "<div style='position:absolute;left:19px;height:28px;display:flex;align-items:center;'>" +
"<label for='width'>Width:</label>&nbsp;<input type='text' name='width' size='4' style='margin-top:-1px;'>" +
"<label for='width'>Width:</label>&nbsp;<input type='text' name='width' size='4' style='margin-top:-2px;'>" +
"</div>" ) ) ;
$width = $btn_pane.find( "input[name='width']" ) ;
}
@ -120,10 +120,7 @@ function _do_edit_simple_note( template_id, player_no, $sortable2, $entry, defau
initTrumbowyg( $caption, gAppConfig.trumbowyg["simple-note-dialog"], $(this) ) ;
else {
// always start non-maximized, and in HTML mode
if ( $caption.parent().hasClass( "trumbowyg-fullscreen" ) )
$caption.trumbowyg( "execCmd", { cmd: "fullscreen" } ) ;
if ( $caption.parent().hasClass( "trumbowyg-editor-hidden" ) )
$caption.trumbowyg( "toggle" ) ;
resetTrumbowyg( $caption ) ;
}
// tweak the SNIPPETS button so that snippets will work
$btn.data( { id: template_id, "player-no": player_no } ) ;
@ -140,7 +137,7 @@ function _do_edit_simple_note( template_id, player_no, $sortable2, $entry, defau
$btn_pane.find( "label[for='width']" ).css( "display", show?"inline":"none" ) ;
$width.css( "display", show?"inline":"none" ) ;
$btn.css( { position: "absolute", left:
show ? $width.offset().left + $width.width() - $btn_pane.find("label[for='width']").offset().left + 25 : 15
show ? $width.offset().left + $width.width() - $btn_pane.find("label[for='width']").offset().left + 32 : 15
} ) ;
// enable auto-dismiss for the dialog
var $dlg = $(this) ;

@ -967,11 +967,19 @@ function unload_snippet_params( unpack_scenario_date, template_id )
if ( template_id === null || template_id.substr(0,7) !== "extras/" )
return ;
}
params[ $elem.attr("name") ] = $elem.hasClass("trumbowyg-editor") ? unloadTrumbowyg($elem,false) : $elem.val() ;
var val ;
if ( $elem.hasClass( "trumbowyg-editor" ) )
val = unloadTrumbowyg( $elem, false ) ;
else if ( $elem.hasClass( "html-textbox" ) )
val = $elem.html() ;
else
val = $elem.val() ;
params[ $elem.attr("name") ] = val ;
} ;
$("input[type='text'].param").each( function() { add_param( $(this) ) ; } ) ;
$(".trumbowyg-editor.param").each( function() { add_param( $(this) ) ; } ) ;
$("select.param").each( function() { add_param( $(this) ) ; } ) ;
$( "input[type='text'].param" ).each( function() { add_param( $(this) ) ; } ) ;
$( "div.html-textbox.param" ).each( function() { add_param( $(this) ) ; } ) ;
$( ".trumbowyg-editor.param" ).each( function() { add_param( $(this) ) ; } ) ;
$( "select.param" ).each( function() { add_param( $(this) ) ; } ) ;
// fix up the turn track parameters
var nTurns = params.TURN_TRACK_NTURNS ;
@ -1518,7 +1526,7 @@ function make_crew_survival( vo_entry )
// check if the vehicle is subject to brew up
var pos = crew_survival.indexOf( ":brewup" ) ;
if ( pos !== -1 ) {
crew_survival = "<span class='brewup'>" +
crew_survival = "<span class=\"brewup\">" +
crew_survival.substring(0,pos) + crew_survival.substring(pos+7) +
"</span>" ;
}
@ -1766,6 +1774,8 @@ function do_load_scenario_data( params )
var val = $elem.prop("disabled") ? "" : params[key] ;
if ( $elem.hasClass( "trumbowyg-editor" ) )
$elem.trumbowyg( "html", val ) ;
else if ( $elem.hasClass( "html-textbox" ) )
$elem.html( val ) ;
else
$elem.val( val ) ;
if ( key === "ASA_ID" )
@ -1875,13 +1885,16 @@ function do_load_scenario_data( params )
continue ;
}
//jshint loopfunc: true
var $elem = $( "input[type='text'][name='" + key + "'].param" ).each( function() {
$( "input[type='text'][name='" + key + "'].param" ).each( function() {
set_param( $(this), key ) ;
} ) ;
$( "div.html-textbox[name='" + key + "'].param" ).each( function() {
set_param( $(this), key ) ;
} ) ;
$elem = $( ".trumbowyg-editor[name='" + key + "'].param" ).each( function() {
$( ".trumbowyg-editor[name='" + key + "'].param" ).each( function() {
set_param( $(this), key ) ;
} ) ;
$elem = $( "select[name='" + key + "'].param" ).each( function() {
$( "select[name='" + key + "'].param" ).each( function() {
if ( key !== "PLAYER_1" && key !== "PLAYER_2" )
set_param( $(this), key ).trigger( "change" ) ;
} ) ;
@ -2008,8 +2021,8 @@ function on_save_scenario()
// generate the save filename
var save_fname = gLastSavedScenarioFilename ;
if ( ! save_fname ) {
var scenario_name = params.SCENARIO_NAME.trim() ;
var scenario_id = params.SCENARIO_ID.trim() ;
var scenario_name = $( "div.html-textbox[name='SCENARIO_NAME']" ).text().trim() ;
var scenario_id = $( "div.html-textbox[name='SCENARIO_ID']" ).text().trim() ;
if ( scenario_name && scenario_id )
save_fname = scenario_name + " (" + scenario_id + ").json" ;
else if ( scenario_name )
@ -2158,12 +2171,13 @@ function do_on_new_scenario( user_requested ) {
function reset_scenario()
{
// reset all the template parameters
$("input[type='text'].param").each( function() {
$( "input[type='text'].param" ).each( function() {
if ( ! $.contains( $("#tabs-extras")[0], $(this)[0] ) )
$(this).val( "" ) ;
} ) ;
$(".trumbowyg-editor").each( function() { $(this).trumbowyg( "empty" ) ; } ) ;
$("input[type='checkbox']").prop( "checked", false ) ;
$( "div.html-textbox.param" ).each( function() { $(this).html( "" ) ; } ) ;
$( ".trumbowyg-editor" ).each( function() { $(this).trumbowyg( "empty" ) ; } ) ;
$( "input[type='checkbox']" ).prop( "checked", false ) ;
$( "select[name='TURN_TRACK_NTURNS'].param" ).val( "" ).trigger( "change" ) ;
// reset the player droplist's
@ -2468,8 +2482,8 @@ function _update_vo_sortable2_entries()
function update_scenario_status()
{
// get the scenario details
var scenario_name = $("input[name='SCENARIO_NAME']").val().trim() ;
var scenario_id = $("input[name='SCENARIO_ID']").val().trim() ;
var scenario_name = $( "div.html-textbox[name='SCENARIO_NAME']" ).text().trim() ;
var scenario_id = $( "div.html-textbox[name='SCENARIO_ID']" ).text().trim() ;
var caption = "" ;
if ( scenario_name && scenario_id )
caption = scenario_name + " (" + scenario_id + ")" ;

@ -213,6 +213,9 @@ function init_dialog( $dlg, ok_button_text, auto_dismiss )
$dlg.find( "input[type='checkbox']" ).keydown( function(evt) {
auto_dismiss_dialog( $dlg, evt, ok_button_text ) ;
} ) ;
$dlg.find( "div.html-textbox" ).keydown( function(evt) {
auto_dismiss_dialog( $dlg, evt, ok_button_text ) ;
} ) ;
$dlg.find( "select" ).keydown( function(evt) {
auto_dismiss_dialog( $dlg, evt, ok_button_text ) ;
} ) ;

@ -202,8 +202,13 @@ function _get_raw_content( snippet_id, $btn, params )
function get_values( names ) {
for ( var i=0 ; i < names.length ; ++i ) {
var $elem = $( ".param[name='" + names[i] + "']" ) ;
var val = $elem.hasClass("trumbowyg-editor") ? unloadTrumbowyg($elem,false) : $elem.val() ;
val = val.trim() ;
var val ;
if ( $elem.hasClass( "trumbowyg-editor" ) )
val = unloadTrumbowyg( $elem, false ) ;
else if ( $elem.hasClass( "html-textbox" ) )
val = $elem.html() ;
else
val = $elem.val() ;
if ( val )
raw_content.push( val ) ;
}

@ -29,12 +29,15 @@ function _do_edit_ob_vo( $entry, player_no, vo_type )
$sortable.sortable2( "delete-all" ) ;
for ( var i=0 ; i < entries.length ; ++i )
add_entry( $sortable, entries[i], false ) ;
$sortable.scrollTop( 0 ) ;
}
function unload_entries( $sortable ) {
var entries = [] ;
$sortable.find( "input[type='text']" ).each( function() {
var val = $(this).val().trim() ;
if ( val )
$sortable.find( "div.html-textbox" ).each( function() {
var val = $(this).html().trim() ;
while ( val.substr( val.length-4 ) === "<br>" )
val = val.substr( 0, val.length-4 ) ;
if ( val.length > 0 )
entries.push( val ) ;
} ) ;
return entries ;
@ -90,21 +93,35 @@ function _do_edit_ob_vo( $entry, player_no, vo_type )
var $capabilities = $( "#vo_capabilities-sortable" ) ;
var $elite = $( "#edit-vo .capabilities input.elite" ) ;
var $comments = $( "#vo_comments-sortable" ) ;
function add_entry( $sortable, val, visible ) {
function add_entry( $sortable, val, focus ) {
var $elem = $( "<div>" +
"<img class='dragger' src='" + gImagesBaseUrl + "/dragger.png'>" +
"<input type='text'>" +
"<div class='html-textbox'></div>" +
"</div>"
) ;
$elem.children( "input[type='text']" ).val( val ).keydown( function(evt) {
var $htmlTextbox = $elem.children( "div.html-textbox" ) ;
$htmlTextbox.html( val ).keydown( function( evt ) {
auto_dismiss_dialog( $dlg, evt, "OK" ) ;
} ) ;
var $entry = $sortable.sortable2( "add", {
content: $elem,
data: { fixed_height: "1.5em" },
} ) ;
if ( visible ) {
$entry.find( "input[type='text']" ).focus() ;
var objName ;
if ( $sortable === $capabilities )
objName = vo_type + " capability" ;
else if ( $sortable === $comments )
objName = vo_type + " comment" ;
initHtmlTextbox( $htmlTextbox, objName, false ) ;
// FUDGE! This works around a weird presentation error if there are superscripts and daggers
// in the content, where ths text won't be vertically aligned properly. Moving the cursor around
// seems to fix the layout, so we move to the start of the content, which will be a "normal" character.
$htmlTextbox.focus().trigger(
{ type: "keypress", keycode: $.ui.keyCode.HOME }
) ;
// check if we should set focus to the entry
if ( focus ) {
$entry.find( "div.html-textbox" ).focus() ;
$entry[0].scrollIntoView() ;
}
}
@ -152,6 +169,7 @@ function _do_edit_ob_vo( $entry, player_no, vo_type )
create: function() {
// initialize the dialog
init_dialog( $(this), "OK", false ) ;
// NOTE: We disable Control-Click deleting entries, because we want that to open the "edit HTML" dialog.
$capabilities.sortable2( "init", {
add: function() { add_entry( $capabilities, "", true ) ; },
reset: on_reset_capabilities,
@ -162,6 +180,14 @@ function _do_edit_ob_vo( $entry, player_no, vo_type )
reset: on_reset_comments,
no_confirm_delete: true,
} ) ;
// FUDGE! Making the entire entry draggable causes problems when clicking inside the HTML textbox
// (presumably because the click event is being eaten somewhere). We can work around this by adding
// a click handler that sets focus to the textbox, but the cursor is always placed at the start
// of the content, not where the mouse was clicked. We work-around the problem by only allow entries
// to be be dragged via the dragger icon.
$capabilities.sortable( "option", "handle", ".dragger" ) ;
$comments.sortable( "option", "handle", ".dragger" ) ;
// handle changes to Elite status
$elite.click( function() {
update_for_elite( $(this).prop( "checked" ) ? +1 : -1 ) ;
} ) ;

@ -0,0 +1,5 @@
<div id="edit-html_textbox-dialog" style="display:none;">
<div class="container">
<div class="content"></div>
</div>
</div>

@ -28,6 +28,7 @@
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/sortable.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/edit-template-dialog.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/edit-simple-note-dialog.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/edit-html-textbox-dialog.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/ask-dialog.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/select-vo-dialog.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/edit-vo-dialog.css')}}" />
@ -84,6 +85,7 @@
{%include "edit-template-dialog.html"%}
{%include "edit-simple-note-dialog.html"%}
{%include "edit-html-textbox-dialog.html"%}
{%include "select-vo-dialog.html"%}
{%include "select-vo-image-dialog.html"%}

@ -6,14 +6,16 @@
<div id="panel-scenario">
<div class="row">
<label for="SCENARIO_NAME">Name:</label>
<input name="SCENARIO_NAME" type="text" class="param" title="Scenario name">
<input name="SCENARIO_ID" type="text" class="param" title="Scenario ID">
<div name="SCENARIO_NAME" class="param html-textbox" title="Scenario name"></div>
<span class="spacer"></span>
<div name="SCENARIO_ID" class="param html-textbox" title="Scenario ID"></div>
<button class="scenario-search" title="Search for scenarios"></button>
</div>
<div class="row">
<label for="SCENARIO_LOCATION">Location:</label>
<input name="SCENARIO_LOCATION" type="text" class="param" title="Scenario location">
<div name="SCENARIO_LOCATION" class="param html-textbox" title="Scenario location"></div>
<select name="SCENARIO_THEATER" class="param" title="Scenario theater"> </select>
<span class="spacer"></span>
<button class="generate" data-id="compass"></button>
<input name="COMPASS" type="text" class="param" style="display:none;width:1em;">
</div>
@ -59,7 +61,7 @@
</div>
<div class="row">
<label for="PLAYER_1_DESCRIPTION"></label>
<input name="PLAYER_1_DESCRIPTION" type="text" class="param" title="Description of the player's OB">
<div name="PLAYER_1_DESCRIPTION" class="param html-textbox" title="Description of the player's OB"></div>
</div>
<div class="row" style='max-width:32em;'>
<label for="PLAYER_2">Player 2:</label>
@ -70,7 +72,7 @@
</div>
<div class="row">
<label for="PLAYER_2_DESCRIPTION"></label>
<input name="PLAYER_2_DESCRIPTION" type="text" class="param" title="Description of the player's OB">
<div name="PLAYER_2_DESCRIPTION" class="param html-textbox" title="Description of the player's OB"></div>
</div>
<div class="row">
<input name="ASA_ID" type="text" size="1" class="param" style="display:none;">

@ -1,15 +1,18 @@
{
"SCENARIO_NAME": "default scenario name",
"SCENARIO_LOCATION": "default location",
"SCENARIO_NAME": "default <i>scenario name</i>",
"SCENARIO_LOCATION": "default <i>location</i>",
"SCENARIO_ID": "sc <u>id</u>",
"SCENARIO_DATE": "2000-12-25",
"SCENARIO_WIDTH": "1px",
"PLAYER_1": "american",
"PLAYER_1_DESCRIPTION": "player 1 <i>description</i>",
"PLAYER_1_ELR": "1",
"PLAYER_1_SAN": "2",
"PLAYER_2": "japanese",
"PLAYER_2_DESCRIPTION": "player 2 <i>description</i>",
"PLAYER_2_ELR": "3",
"PLAYER_2_SAN": "4",

@ -4,12 +4,11 @@ import re
import pytest
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from vasl_templates.webapp.tests.utils import \
init_webapp, select_menu_option, select_tab, click_dialog_button, \
init_webapp, select_menu_option, select_tab, click_dialog_button, drag_sortable_entry_to_trash, \
find_child, find_children, wait_for_clipboard, \
set_scenario_date
set_scenario_date, load_html_textbox
from vasl_templates.webapp.tests import pytest_options
from vasl_templates.webapp.tests.test_vo_reports import get_vo_report
from vasl_templates.webapp.tests.test_vehicles_ordnance import add_vo
@ -509,8 +508,8 @@ def test_custom_capabilities( webapp, webdriver ): #pylint: disable=too-many-sta
def check_capabilities_in_dialog( expected ):
"""Check the vehicle's capabilities."""
elems = find_children( "#vo_capabilities-sortable li" )
elems2 = [ find_child("input[type='text']",c) for c in elems ]
assert [ e.get_attribute("value") for e in elems2 ] == expected
elems2 = [ find_child( "div.html-textbox", c ) for c in elems ]
assert [ e.get_attribute( "innerHTML" ) for e in elems2 ] == expected
return elems
# check the vehicle's snippet
@ -521,23 +520,24 @@ def test_custom_capabilities( webapp, webdriver ): #pylint: disable=too-many-sta
elems = find_children( "li", vehicles_sortable )
assert len(elems) == 1
ActionChains( webdriver ).double_click( elems[0] ).perform()
elems = check_capabilities_in_dialog( [ "XYZ", "<span class='brewup'>cs 4</span>" ] )
elems = check_capabilities_in_dialog( [ "XYZ", "<span class=\"brewup\">cs 4</span>" ] )
# edit one of the capabilities
elem = find_child( "input[type='text']", elems[0] )
elem = find_child( "div.html-textbox", elems[0] )
elem.clear()
elem.send_keys( "XYZ (modified)" )
# delete a capability
ActionChains( webdriver ).key_down( Keys.CONTROL ).click( elems[1] ).perform()
ActionChains( webdriver ).key_up( Keys.CONTROL ).perform()
sortable = find_child( "#vo_capabilities-sortable" )
drag_sortable_entry_to_trash( sortable, 1, ".dragger" )
# add a new capability
elem = find_child( "#vo_capabilities-add" )
elem.click()
elems = find_children( "#vo_capabilities-sortable input[type='text']" )
elems = find_children( "#vo_capabilities-sortable div.html-textbox" )
assert len(elems) == 2
elems[1].send_keys( "a <i>new</i> capability" )
htb = find_children( "div.html-textbox", sortable )[1]
load_html_textbox( htb, "a <i>new</i> capability" )
# save the changes and check the vehicle's snippet
click_dialog_button( "OK" )
@ -561,9 +561,9 @@ def test_custom_capabilities( webapp, webdriver ): #pylint: disable=too-many-sta
elems = check_capabilities_in_dialog( [ "XYZ (modified)", "a <i>new</i> capability" ] )
# delete all capabilities
for elem in elems:
ActionChains( webdriver ).key_down( Keys.CONTROL ).click( elem ).perform()
ActionChains( webdriver ).key_up( Keys.CONTROL ).perform()
sortable = find_child( "#vo_capabilities-sortable" )
for _ in range( len(elems) ):
drag_sortable_entry_to_trash( sortable, 0, ".dragger" )
click_dialog_button( "OK" )
check_snippet( "" )
@ -594,12 +594,11 @@ def test_custom_capabilities( webapp, webdriver ): #pylint: disable=too-many-sta
elems = find_children( "li", vehicles_sortable )
assert len(elems) == 1
ActionChains( webdriver ).double_click( elems[0] ).perform()
elems = find_children( "#vo_capabilities-sortable input[type='text']" )
elems = find_children( "#vo_capabilities-sortable div.html-textbox" )
assert len(elems) == 2
elems[0].clear()
elems[0].send_keys( "XYZ" )
elems[1].clear()
elems[1].send_keys( "<span class='brewup'>cs 4</span>" )
load_html_textbox( elems[1], "<span class=\"brewup\">cs 4</span>" )
click_dialog_button( "OK" )
# make sure the custom capabilities are no longer saved in the scenario
@ -633,8 +632,8 @@ def test_custom_comments( webapp, webdriver ): #pylint: disable=too-many-stateme
def check_comments_in_dialog( expected ):
"""Check the vehicle's comments."""
elems = find_children( "#vo_comments-sortable li" )
elems2 = [ find_child("input[type='text']",c) for c in elems ]
assert [ e.get_attribute("value") for e in elems2 ] == expected
elems2 = [ find_child( "div.html-textbox", c ) for c in elems ]
assert [ e.get_attribute( "innerHTML" ) for e in elems2 ] == expected
return elems
# check the vehicle's snippet
@ -648,20 +647,21 @@ def test_custom_comments( webapp, webdriver ): #pylint: disable=too-many-stateme
elems = check_comments_in_dialog( [ "a comment", "another comment" ] )
# edit one of the comments
elem = find_child( "input[type='text']", elems[0] )
elem = find_child( "div.html-textbox", elems[0] )
elem.clear()
elem.send_keys( "a comment (modified)" )
# delete a comment
ActionChains( webdriver ).key_down( Keys.CONTROL ).click( elems[1] ).perform()
ActionChains( webdriver ).key_up( Keys.CONTROL ).perform()
sortable = find_child( "#vo_comments-sortable" )
drag_sortable_entry_to_trash( sortable, 1, ".dragger" )
# add a new comment
elem = find_child( "#vo_comments-add" )
elem.click()
elems = find_children( "#vo_comments-sortable input[type='text']" )
elems = find_children( "#vo_comments-sortable div.html-textbox" )
assert len(elems) == 2
elems[1].send_keys( "a <i>new</i> comment" )
htb = find_children( "div.html-textbox", sortable )[1]
load_html_textbox( htb, "a <i>new</i> comment" )
# save the changes and check the vehicle's snippet
click_dialog_button( "OK" )
@ -685,9 +685,9 @@ def test_custom_comments( webapp, webdriver ): #pylint: disable=too-many-stateme
elems = check_comments_in_dialog( [ "a comment (modified)", "a <i>new</i> comment" ] )
# delete all comments
for elem in elems:
ActionChains( webdriver ).key_down( Keys.CONTROL ).click( elem ).perform()
ActionChains( webdriver ).key_up( Keys.CONTROL ).perform()
sortable = find_child( "#vo_comments-sortable" )
for _ in range( len(elems) ):
drag_sortable_entry_to_trash( sortable, 0, ".dragger" )
click_dialog_button( "OK" )
check_snippet( "" )
@ -718,7 +718,7 @@ def test_custom_comments( webapp, webdriver ): #pylint: disable=too-many-stateme
elems = find_children( "li", vehicles_sortable )
assert len(elems) == 1
ActionChains( webdriver ).double_click( elems[0] ).perform()
elems = find_children( "#vo_comments-sortable input[type='text']" )
elems = find_children( "#vo_comments-sortable div.html-textbox" )
assert len(elems) == 2
elems[0].clear()
elems[0].send_keys( "a comment" )
@ -795,7 +795,7 @@ def test_capability_updates_in_ui( webapp, webdriver ):
ActionChains( webdriver ).double_click( elems[1] ).perform()
elem = find_child( "#vo_capabilities-add" )
elem.click()
elems = find_children( "#vo_capabilities-sortable input[type='text']" )
elems = find_children( "#vo_capabilities-sortable div.html-textbox" )
assert len(elems) == 4
elems[3].send_keys( "foo!" )
click_dialog_button( "OK" )
@ -890,7 +890,10 @@ def test_elite( webapp, webdriver ): #pylint: disable=too-many-statements
def check_elite2( expected, custom ):
"""Check the elite status of the vehicle in the edit dialog."""
vo_name = find_child( "#edit-vo .header .vo-name" ).text
caps = [ c.get_attribute("value") for c in find_children("#vo_capabilities-sortable input[type='text']") ]
caps = [
c.get_attribute( "innerHTML" )
for c in find_children( "#vo_capabilities-sortable div.html-textbox" )
]
if expected:
assert vo_name.endswith( "\u24ba" )
expected = [ "H9", "s10", "sD7", "CS 5" ]
@ -919,7 +922,7 @@ def test_elite( webapp, webdriver ): #pylint: disable=too-many-statements
ActionChains( webdriver ).double_click( get_sortable_elem() ).perform()
elem = find_child( "#vo_capabilities-add" )
elem.click()
elems = find_children( "#vo_capabilities-sortable input[type='text']" )
elems = find_children( "#vo_capabilities-sortable div.html-textbox" )
assert len(elems) == 5
elems[4].send_keys( "HE10" )
click_dialog_button( "OK" )
@ -966,10 +969,10 @@ def test_elite( webapp, webdriver ): #pylint: disable=too-many-statements
elem = find_child( "#edit-vo .capabilities .elite" )
elem.click()
check_elite2( True, True )
elems = find_children( "#vo_capabilities-sortable li" )
sortable = find_child( "#vo_capabilities-sortable" )
elems = find_children( "li", sortable )
webdriver.execute_script( "arguments[0].scrollIntoView(true);", elems[4] )
ActionChains( webdriver ).key_down( Keys.CONTROL ).click( elems[4] ).perform()
ActionChains( webdriver ).key_up( Keys.CONTROL ).perform()
drag_sortable_entry_to_trash( sortable, 4, ".dragger" )
click_dialog_button( "OK" )
check_elite( True, False )

@ -16,13 +16,17 @@ def test_default_scenario( webapp, webdriver ):
init_webapp( webapp, webdriver )
# wait for the scenario to load
elem = find_child( "input[name='SCENARIO_NAME']" )
wait_for( 5, lambda: elem.get_attribute("value") != "" )
elem = find_child( "div.html-textbox[name='SCENARIO_NAME']" )
wait_for( 5, lambda: elem.get_attribute( "innerHTML" ) != "" )
def check_textbox( field_name, expected ):
"""Check that a field has been loaded correctly."""
elem = find_child( "input[name='{}']".format( field_name ) )
assert elem.get_attribute( "value" ) == expected
def check_html_textbox( field_name, expected ):
"""Check that a field has been loaded correctly."""
elem = find_child( "div.html-textbox[name='{}']".format( field_name ) )
assert elem.get_attribute( "innerHTML" ) == expected
def check_trumbowyg( field_name, expected ):
"""Check that a field has been loaded correctly."""
assert unload_trumbowyg( field_name ) == expected
@ -34,16 +38,19 @@ def test_default_scenario( webapp, webdriver ):
select_tab( "scenario" )
# check the scenario fields
check_textbox( "SCENARIO_NAME", "default scenario name" )
check_textbox( "SCENARIO_LOCATION", "default location" )
check_html_textbox( "SCENARIO_NAME", "default <i>scenario name</i>" )
check_html_textbox( "SCENARIO_LOCATION", "default <i>location</i>" )
check_html_textbox( "SCENARIO_ID", "sc <u>id</u>" )
check_textbox( "SCENARIO_DATE", "12/25/2000" )
check_textbox( "SCENARIO_WIDTH", "1px" )
# check the player fields
check_droplist( "PLAYER_1", "american" )
check_html_textbox( "PLAYER_1_DESCRIPTION", "player 1 <i>description</i>" )
check_droplist( "PLAYER_1_ELR", "1" )
check_droplist( "PLAYER_1_SAN", "2" )
check_droplist( "PLAYER_2", "japanese" )
check_html_textbox( "PLAYER_2_DESCRIPTION", "player 2 <i>description</i>" )
check_droplist( "PLAYER_2_ELR", "3" )
check_droplist( "PLAYER_2_SAN", "4" )
check_textbox( "PLAYERS_WIDTH", "" )

@ -60,18 +60,25 @@ def test_dirty_scenario_checks( webapp, webdriver ): #pylint: disable=too-many-s
else:
webdriver.execute_script( "arguments[0].value = arguments[1]", target, new_val )
return target, prev_val, new_val
elif target.tag_name == "div":
assert "trumbowyg-editor" in get_css_classes( target )
prev_val = unload_trumbowyg( target )
new_val = "changed value"
load_trumbowyg( target, new_val )
return target, prev_val, new_val
elif target.tag_name == "select":
sel = Select( target )
prev_val = sel.first_selected_option.get_attribute( "value" )
select_droplist_index( sel, 2 )
new_val = sel.first_selected_option.get_attribute( "value" )
return target, prev_val, new_val
else:
css_classes = get_css_classes( target )
if "trumbowyg-editor" in css_classes:
prev_val = unload_trumbowyg( target )
new_val = "changed value"
load_trumbowyg( target, new_val )
return target, prev_val, new_val
elif "html-textbox" in css_classes:
prev_val = target.get_attribute( "innerHTML" )
new_val = "changed value"
target.clear()
target.send_keys( new_val )
return target, prev_val, new_val
assert False
return None
@ -83,13 +90,19 @@ def test_dirty_scenario_checks( webapp, webdriver ): #pylint: disable=too-many-s
assert get_sortable_entry_count( state ) == 1
elif state[0].tag_name == "input":
assert state[0].get_attribute("value") == state[2]
elif state[0].tag_name == "div":
assert "trumbowyg-editor" in get_css_classes( state[0] )
assert unload_trumbowyg( state[0] ) == state[2]
elif state[0].tag_name == "select":
assert Select(state[0]).first_selected_option.get_attribute("value") == state[2]
else:
assert False
css_classes = get_css_classes( state[0] )
if "trumbowyg-editor" in css_classes:
assert unload_trumbowyg( state[0] ) == state[2]
elif "html-textbox" in css_classes:
val = state[0].get_attribute( "innerHTML" )
if val.endswith( "<br>" ):
val = val[:-4] # nb: for Firefox
assert val == state[2]
else:
assert False
def revert_field( param, state ):
"""Revert a change we made to a field."""
@ -103,13 +116,17 @@ def test_dirty_scenario_checks( webapp, webdriver ): #pylint: disable=too-many-s
state[0].send_keys( state[1] )
else:
webdriver.execute_script( "arguments[0].value = arguments[1]", state[0], state[1] )
elif state[0].tag_name == "div":
assert "trumbowyg-editor" in get_css_classes( state[0] )
load_trumbowyg( state[0], state[1] )
elif state[0].tag_name == "select":
select_droplist_val( Select(state[0]), state[1] )
else:
assert False
css_classes = get_css_classes( state[0] )
if "trumbowyg-editor" in css_classes:
load_trumbowyg( state[0], state[1] )
elif "html-textbox" in css_classes:
state[0].clear()
state[0].send_keys( state[1] )
else:
assert False
def check_is_dirty( expected ):
"""Check if the scenario is being flagged as dirty."""

@ -1,9 +1,10 @@
""" Test sanitizing HTML. """
""" Test HTML-related functionality. """
import os
import re
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from vasl_templates.webapp.tests.utils import \
init_webapp, select_tab, click_dialog_button, load_trumbowyg, unload_trumbowyg, \
@ -29,6 +30,9 @@ def test_sanitize_load_scenario( webapp, webdriver ):
def check_val( name, expected ):
elem = find_child( ".param[name='{}']".format( name ) )
assert elem.get_attribute( "value" ) == expected
def check_html_textbox( name, expected ):
elem = find_child( "div.html-textbox[name='{}']".format( name ) )
assert elem.get_attribute( "innerHTML" ) == expected
def check_trumbowyg( name, expected ):
assert unload_trumbowyg( name ) == expected
def check_sortable( sortable_sel, expected, expected_width ):
@ -43,18 +47,18 @@ def test_sanitize_load_scenario( webapp, webdriver ):
def check_custom_cap( sortable_sel, expected ):
elem = find_child( "{} li".format( sortable_sel ) ) # nb: we assume there's only 1 entry
ActionChains( webdriver ).double_click( elem ).perform()
elem = find_child( ".ui-dialog.edit-vo .sortable input" )
assert elem.get_attribute( "value" ) == expected
elem = find_child( ".ui-dialog.edit-vo .sortable div.html-textbox" )
assert elem.get_attribute( "innerHTML" ) == expected
click_dialog_button( "Cancel" )
# check what was loaded into the UI
# NOTE: We can't use save_scenario), since that also sanitizes HTML.
check_val( "SCENARIO_NAME", "!scenario_name:#" )
check_val( "SCENARIO_ID", "!scenario_id:@@@#" )
check_val( "SCENARIO_LOCATION", "!scenario_location:<div style=\"text-align:right;\">@@@</div>#" )
check_html_textbox( "SCENARIO_NAME", "!scenario_name:#" )
check_html_textbox( "SCENARIO_ID", "!scenario_id:@@@#" )
check_html_textbox( "SCENARIO_LOCATION", "!scenario_location:<div style=\"text-align:right;\">@@@</div>#" )
check_val( "SCENARIO_WIDTH", "!scenario_width:@@@#" )
check_val( "PLAYER_1_DESCRIPTION", "!player1_description:#" )
check_val( "PLAYER_2_DESCRIPTION", "!player2_description:#" )
check_html_textbox( "PLAYER_1_DESCRIPTION", "!player1_description:#" )
check_html_textbox( "PLAYER_2_DESCRIPTION", "!player2_description:#" )
check_val( "PLAYERS_WIDTH", "!players_width:@@@#" )
check_trumbowyg( "VICTORY_CONDITIONS", "!victory_conditions:@@@#" )
check_val( "VICTORY_CONDITIONS_WIDTH", "!victory_conditions_width:@@@#" )
@ -286,6 +290,44 @@ def test_sanitize_input( webapp, webdriver ):
# ---------------------------------------------------------------------
def test_html_textbox( webapp, webdriver ):
"""Test HTML textbox's."""
# initialize
init_webapp( webapp, webdriver )
def transform( clipboard ):
mo = re.search( r"\[(.*?)\]", clipboard )
return mo.group(1)
# enter some plain text into an HTML textbox, then check its contents
ctrl = find_child( "div.html-textbox[name='SCENARIO_NAME']" )
ctrl.send_keys( "abc" )
snippet_btn = find_child( "button.generate[data-id='scenario']" )
snippet_btn.click()
wait_for_clipboard( 2, "abc", transform=transform )
# open the dialog to add some HTML content, then check its contents
ActionChains( webdriver ).key_down( Keys.ALT ).click( ctrl ).perform()
ActionChains( webdriver ).key_up( Keys.ALT ).perform()
dlg = wait_for_elem( 2, ".ui-dialog.edit-html_textbox" )
elem = find_child( ".trumbowyg-editor", dlg )
elem.send_keys( Keys.END )
elem.send_keys( " " )
find_child( "button.trumbowyg-strong-button", dlg ).click()
elem.send_keys( "bold" )
click_dialog_button( "OK" )
snippet_btn.click()
wait_for_clipboard( 2, "abc <b>bold</b>", transform=transform )
# modify the HTML textbox directly, then check its contents
ctrl.send_keys( Keys.END )
ctrl.send_keys( "!!!" )
snippet_btn.click()
wait_for_clipboard( 2, "abc <b>bold!!!</b>", transform=transform )
# ---------------------------------------------------------------------
def _make_scenario_params( real_vo ):
"""Generate scenario parameters that contain unsafe HTML."""

@ -195,11 +195,14 @@ def test_scenario_persistence( webapp, webdriver ): #pylint: disable=too-many-st
elem = find_child( ".param[name='{}']".format( field ) )
if elem.tag_name == "select":
assert Select(elem).first_selected_option.get_attribute("value") == val
elif elem.tag_name == "div":
assert "trumbowyg-editor" in get_css_classes( elem )
assert unload_trumbowyg( elem ) == val
else:
assert elem.get_attribute("value") == val
css_classes = get_css_classes( elem )
if "trumbowyg-editor" in css_classes:
assert unload_trumbowyg( elem ) == val
elif "html-textbox" in css_classes:
assert elem.get_attribute( "innerHTML" ) == val
else:
assert elem.get_attribute("value") == val
select_tab( "scenario" )
scenario_notes = [ c.text for c in find_children("#scenario_notes-sortable li") ]
assert scenario_notes == [ sn["caption"] for sn in SCENARIO_PARAMS["scenario"]["SCENARIO_NOTES"] ]

@ -111,8 +111,8 @@ def test_import_scenario( webapp, webdriver ):
def _check_scenario( **kwargs ):
"""Check the scenario import."""
for key in ["SCENARIO_NAME","SCENARIO_ID","SCENARIO_LOCATION","PLAYER_1_DESCRIPTION","PLAYER_2_DESCRIPTION"]:
elem = find_child( "input[name='{}']".format( key ) )
if elem.get_attribute( "value" ) != kwargs[ key ]:
elem = find_child( "div.html-textbox[name='{}']".format( key ) )
if elem.get_attribute( "innerHTML" ) != kwargs[ key ]:
return False
if get_player_nat( 1 ) != kwargs["PLAYER_1"] or get_player_nat( 2 ) != kwargs["PLAYER_2"]:
return False
@ -161,7 +161,10 @@ def test_import_warnings( webapp, webdriver ): #pylint: disable=too-many-stateme
wait_for( 2, lambda: not find_child( ".warnings", dlg ).is_displayed() )
# do the import again, and accept it
_import_scenario_and_confirm( dlg )
assert elem.get_attribute( "value" ) == expected_val
if "html-textbox" in get_css_classes( elem ):
assert elem.get_attribute( "innerHTML" ) == expected_val
else:
assert elem.get_attribute( "value" ) == expected_val
else:
# nope - check that the import was done
wait_for( 2, lambda: not dlg.is_displayed() )

@ -87,7 +87,10 @@ def test_scenario_snippets( webapp, webdriver ):
"SCENARIO_LOCATION": "right <u>here</u>",
"SCENARIO_DATE": "01/02/1942",
},
'name = [my <i>cool</i> scenario] | loc = [right <u>here</u>] | date = [01/02/1942] aka "2 January, 1942"',
# NOTE: Since we load the UI using send_keys(), content is interpreted as plain-text.
'name = [my &lt;i&gt;cool&lt;/i&gt; scenario]'
' | loc = [right &lt;u&gt;here&lt;/u&gt;]'
' | date = [01/02/1942] aka "2 January, 1942"',
None
)

@ -271,8 +271,7 @@ def set_template_params( params ): #pylint: disable=too-many-branches
elem = find_child( ".param[name='{}']".format( key ) )
if elem.tag_name == "select":
select_droplist_val( Select(elem), val )
elif elem.tag_name == "div":
assert "trumbowyg-editor" in get_css_classes( elem )
elif "trumbowyg-editor" in get_css_classes( elem ):
load_trumbowyg( elem, val )
else:
if elem.is_displayed():
@ -404,11 +403,14 @@ def generate_sortable_entry_snippet( sortable, entry_no ):
elems[entry_no].click()
return _get_clipboard()
def drag_sortable_entry_to_trash( sortable, entry_no ):
def drag_sortable_entry_to_trash( sortable, entry_no, sel=None ):
"""Draw a sortable entry to the trash."""
trash = find_sortable_helper( sortable, "trash" )
elems = find_children( "li", sortable )
ActionChains(_webdriver).drag_and_drop( elems[entry_no], trash ).perform()
elem = elems[ entry_no ]
if sel:
elem = find_child( sel, elem )
ActionChains(_webdriver).drag_and_drop( elem, trash ).perform()
def find_sortable_helper( sortable, tag ):
"""Find a sortable's helper element."""
@ -570,6 +572,15 @@ def get_trumbowyg_editor( ctrl ):
assert False
return None
def load_html_textbox( ctrl, val ):
"""Load an HTML textbox."""
htb_id = ctrl.get_attribute( "data-htb-id" )
find_child( ".edit-html-textbox[data-htb-id='{}']".format( htb_id ) ).click()
dlg = wait_for_elem( 2, ".ui-dialog.edit-html_textbox" )
load_trumbowyg( find_child( ".content", dlg ), val )
click_dialog_button( "OK", "edit-html_textbox" )
wait_for( 2, lambda: ctrl.get_attribute( "innerHTML" ) == val )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def dismiss_notifications():

Loading…
Cancel
Save