parent
fe45cb6170
commit
68ba40d388
@ -0,0 +1,106 @@ |
||||
import { gMainApp, gAppConfig, gEventBus } from "./MainApp.js" ; |
||||
|
||||
let gCollapsibleStates = {} ; // nb: we only save the states for the current session
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
gMainApp.component( "collapser", { |
||||
|
||||
props: [ "collapserId" ], |
||||
data() { return { |
||||
// NOTE: We have to track the collapsed/expanded state here, since there may not be
|
||||
// an associated collapsible element (it's optional).
|
||||
// NOTE: This is a tri-state variable (null = don't show)
|
||||
isCollapsed: null, |
||||
// NOTE: We would normally like to have "target" as a property, but a component's $refs
|
||||
// is only populated after it has been mounted, so you can't have one sub-component refer
|
||||
// to another one (via its ref) in the template, so the link has to be made in the code,
|
||||
// in the component's mounted() handler :-/
|
||||
target: null, |
||||
} ; }, |
||||
|
||||
template: `<img v-if="isCollapsed != null" :src=getImageUrl() @click=onClick class="collapser" />`, |
||||
|
||||
methods: { |
||||
|
||||
initCollapser( collapsible, isCollapsed ) { |
||||
|
||||
// initialize
|
||||
this.collapsible = collapsible ; // nb: an associated collapsible is optional
|
||||
if ( isCollapsed != null ) { |
||||
// nb: the caller has decided whether or not we should be visible
|
||||
this.isCollapsed = isCollapsed ; |
||||
} else if ( collapsible ) { |
||||
// figure out whether or not we should show ourself (based on how much content there is)
|
||||
let content = $( collapsible.$el ).text() ; |
||||
let threshold = gAppConfig.WEBAPP_COLLAPSIBLE_THRESHOLD || 100 ; |
||||
this.isCollapsed = (content.length >= threshold) ? false : null ; |
||||
} |
||||
|
||||
// restore any previously-saved state
|
||||
if ( this.isCollapsed != null ) { |
||||
if ( this.collapserId !== undefined && gCollapsibleStates[this.collapserId] !== undefined ) { |
||||
this.isCollapsed = gCollapsibleStates[ this.collapserId ] ; |
||||
this.updateCollapsible() ; |
||||
} |
||||
} |
||||
|
||||
}, |
||||
|
||||
onClick() { |
||||
// toggle our collapsed state
|
||||
this.isCollapsed = ! this.isCollapsed ; |
||||
this.updateCollapsible() ; |
||||
if ( this.collapserId !== undefined ) { |
||||
// save the new state
|
||||
gCollapsibleStates[ this.collapserId ] = this.isCollapsed ; |
||||
} |
||||
gEventBus.emit( "collapsible-toggled", this ) ; |
||||
}, |
||||
|
||||
getImageUrl() { |
||||
return gImagesBaseUrl //eslint-disable-line no-undef
|
||||
+ "collapser-" + (this.isCollapsed ? "down" : "up") + ".png" ; |
||||
}, |
||||
|
||||
updateCollapsible() { |
||||
// force the associated collapsible to update itself
|
||||
if ( this.collapsible ) |
||||
this.collapsible.collapser = this ; |
||||
}, |
||||
|
||||
}, |
||||
|
||||
} ) ; |
||||
// --------------------------------------------------------------------
|
||||
|
||||
gMainApp.component( "collapsible", { |
||||
|
||||
props: [ "collapsedHeight" ], |
||||
data() { return { |
||||
collapser: null, |
||||
} ; }, |
||||
|
||||
template: ` |
||||
<div :style="{height: isCollapsed() ? getCollapsedHeight()+'px' : null}" |
||||
:class="{collapsed: isCollapsed()}" class="collapsible" |
||||
> |
||||
<slot /> |
||||
</div>`, |
||||
|
||||
methods: { |
||||
|
||||
getCollapsedHeight() { |
||||
if ( this.collapsedHeight !== undefined ) |
||||
return this.collapsedHeight ; |
||||
else |
||||
return gAppConfig.WEBAPP_COLLAPSIBLE_HEIGHT || 50 ; |
||||
}, |
||||
|
||||
isCollapsed() { |
||||
return this.collapser != null && this.collapser.isCollapsed ; |
||||
}, |
||||
|
||||
}, |
||||
|
||||
} ) ; |
@ -0,0 +1,4 @@ |
||||
.collapser { cursor: pointer ; opacity: 0.3 ; } |
||||
|
||||
.collapsible { overflow-y: hidden ; } |
||||
.collapsible.collapsed { mask-image: linear-gradient( to bottom, black 25%, transparent 100% ) ; } |
Before Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 3.0 KiB |
@ -0,0 +1,11 @@ |
||||
[ |
||||
|
||||
{ "ruleid": "C.1", |
||||
"content": "A short user annotation." |
||||
}, |
||||
|
||||
{ "ruleid": "C.2", |
||||
"content": "A long user annotation: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." |
||||
} |
||||
|
||||
] |
@ -0,0 +1,19 @@ |
||||
{ |
||||
|
||||
"chapters": [ |
||||
|
||||
{ "caption": "No content", "chapter_id": "no-content" }, |
||||
{ "caption": "Short content", "chapter_id": "short-content", |
||||
"sections": [ |
||||
{ "caption": "Short content" } |
||||
] |
||||
}, |
||||
{ "caption": "Long content", "chapter_id": "long-content", |
||||
"sections": [ |
||||
{ "caption": "Long content" } |
||||
] |
||||
} |
||||
|
||||
] |
||||
|
||||
} |
@ -0,0 +1,3 @@ |
||||
A long preamble. |
||||
|
||||
<p> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. |
@ -0,0 +1,3 @@ |
||||
A long ASOP entry. |
||||
|
||||
<p> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. |
@ -0,0 +1 @@ |
||||
A short preamble. |
@ -0,0 +1 @@ |
||||
A short ASOP entry. |
@ -0,0 +1,44 @@ |
||||
[ |
||||
|
||||
{ "title": "Backblast", |
||||
"ruleids": [ "C13.8" ], |
||||
"rulerefs": [ |
||||
{ "caption": "HEAT", "ruleids": [ "C13.8" ] }, |
||||
{ "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" ] }, |
||||
{ "ruleids": [ "C10.3" ] }, |
||||
{ "caption": "NA in Advance Phase", "ruleids": [ "A4.7" ] }, |
||||
{ "caption": "'S?' is \"<NA>\"" } |
||||
] |
||||
}, |
||||
|
||||
{ "title": "Firepower", |
||||
"content": "The U.S. has lots of this.", |
||||
"ruleids": [ "A1.21" ], |
||||
"see_also": [ "FP" ] |
||||
}, |
||||
|
||||
{ "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" ] |
||||
} |
||||
|
||||
] |
@ -0,0 +1,12 @@ |
||||
{ |
||||
|
||||
"A.1": { "caption": "A short Q+A" }, |
||||
"A.2": { "caption": "A long Q+A" }, |
||||
|
||||
"B.1": { "caption": "A short errata." }, |
||||
"B.2": { "caption": "A long errata." }, |
||||
|
||||
"C.1": { "caption": "A short user annotation." }, |
||||
"C.2": { "caption": "A long user annotation." } |
||||
|
||||
} |
@ -0,0 +1,11 @@ |
||||
[ |
||||
|
||||
{ "ruleid": "B.1", |
||||
"content": "A short errata." |
||||
}, |
||||
|
||||
{ "ruleid": "B.2", |
||||
"content": "A long errata: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." |
||||
} |
||||
|
||||
] |
@ -0,0 +1,25 @@ |
||||
{ |
||||
|
||||
"test": [ |
||||
|
||||
{ "caption": "A short Q+A", |
||||
"ruleids": [ "A.1" ], |
||||
"content": [ |
||||
{ "question": "Some short content.", |
||||
"answers": [ ["Yes.","test"] ] |
||||
} |
||||
] |
||||
}, |
||||
|
||||
{ "caption": "A long Q+A", |
||||
"ruleids": [ "A.2" ], |
||||
"content": [ |
||||
{ "question": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", |
||||
"answers": [ ["No.","test"] ] |
||||
} |
||||
] |
||||
} |
||||
|
||||
] |
||||
|
||||
} |
@ -0,0 +1,223 @@ |
||||
""" Test collapsible's and collaper's. """ |
||||
|
||||
from asl_rulebook2.webapp.tests.utils import init_webapp, select_tabbed_page, \ |
||||
find_child, find_children, wait_for, wait_for_elem |
||||
|
||||
from asl_rulebook2.webapp.tests.test_search import do_search |
||||
from asl_rulebook2.webapp.tests.test_asop import unload_asop_nav, open_asop_chapter |
||||
from asl_rulebook2.webapp.tests.utils import has_class |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_index_sr( webapp, webdriver ): |
||||
"""Test collapsible index search results.""" |
||||
|
||||
# initialize |
||||
webapp.control_tests.set_data_dir( "collapsible" ) |
||||
init_webapp( webapp, webdriver ) |
||||
|
||||
def count_rulerefs( sr_elem ): |
||||
# return how many ruleref's the index search result has |
||||
rulerefs = find_children( "ul.rulerefs li", sr_elem ) |
||||
return len( [ r for r in rulerefs if r.is_displayed() ] ) |
||||
|
||||
def do_test( query_string, expected_state, expected_rulerefs, expected_rulerefs2 ): |
||||
|
||||
# do the search |
||||
results = do_search( query_string ) |
||||
assert len(results) == 1 |
||||
|
||||
# check the collapsed/expanded state |
||||
sr_elem = find_child( "#search-results .sr" ) |
||||
assert _is_collapsed( sr_elem, has_collapsible=False ) == expected_state |
||||
|
||||
# check the number of rulerefs |
||||
assert count_rulerefs( sr_elem ) == expected_rulerefs |
||||
|
||||
if expected_state is None: |
||||
return |
||||
|
||||
# toggle the state and check the results |
||||
find_child( ".collapser", sr_elem ).click() |
||||
wait_for( 2, lambda: _is_collapsed( sr_elem, has_collapsible=False ) != expected_state ) |
||||
assert count_rulerefs( sr_elem ) == expected_rulerefs2 |
||||
|
||||
# toggle the state back and check the results |
||||
find_child( ".collapser", sr_elem ).click() |
||||
wait_for( 2, lambda: _is_collapsed( sr_elem, has_collapsible=False ) == expected_state ) |
||||
assert count_rulerefs( sr_elem ) == expected_rulerefs |
||||
|
||||
# do the tests |
||||
do_test( "CCPh", False, 2, 0 ) # matches the title |
||||
do_test( "Combat", False, 2, 0 ) # matches the subtitle |
||||
do_test( "running", False, 4, 0 ) # matches the content |
||||
do_test( "RCL", True, 1, 2 ) # matches some (but not all) of the ruleref's |
||||
do_test( "rcl AND heat", None, 2, None ) # matches all of the ruleref's |
||||
do_test( "firepower", None, 0, None ) # has no ruleref's |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_qa( webapp, webdriver ): |
||||
"""Test collapsible Q+A entries.""" |
||||
|
||||
# initialize |
||||
webapp.control_tests.set_data_dir( "collapsible" ) |
||||
init_webapp( webapp, webdriver ) |
||||
|
||||
# do the tests |
||||
_test_collapsible( '"short Q+A"', "A.1", None ) |
||||
_test_collapsible( '"long Q+A"', "A.2", False ) |
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
def test_errata( webapp, webdriver ): |
||||
"""Test collapsible errata.""" |
||||
|
||||
# initialize |
||||
webapp.control_tests.set_data_dir( "collapsible" ) |
||||
init_webapp( webapp, webdriver ) |
||||
|
||||
# do the tests |
||||
_test_collapsible( '"short errata"', "B.1", None ) |
||||
_test_collapsible( '"long errata"', "B.2", False ) |
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
def test_user_annotations( webapp, webdriver ): |
||||
"""Test collapsible user annotations.""" |
||||
|
||||
# initialize |
||||
webapp.control_tests.set_data_dir( "collapsible" ) |
||||
init_webapp( webapp, webdriver ) |
||||
|
||||
# do the tests |
||||
_test_collapsible( '"short user annotations"', "C.1", None ) |
||||
_test_collapsible( '"long user annotations"', "C.2", False ) |
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
def test_asop_sr( webapp, webdriver ): |
||||
"""Test ASOP search results.""" |
||||
|
||||
# initialize |
||||
webapp.control_tests.set_data_dir( "collapsible" ) |
||||
init_webapp( webapp, webdriver ) |
||||
|
||||
# do the tests |
||||
_test_collapsible( '"short ASOP"', None, None ) |
||||
_test_collapsible( '"long ASOP"', None, False ) |
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
def _test_collapsible( query_string, ruleid, expected ): |
||||
|
||||
# do the search |
||||
if find_child( "#rule-info" ).is_displayed(): |
||||
find_child( ".close-rule-info" ).click() |
||||
results = do_search( query_string ) |
||||
assert len(results) == 1 |
||||
sr_elem = find_child( "#search-results .sr" ) |
||||
assert _is_collapsed( sr_elem ) == expected |
||||
|
||||
# toggle the state and check the results |
||||
if expected is not None: |
||||
find_child( ".collapser", sr_elem ).click() |
||||
wait_for( 2, lambda: _is_collapsed( sr_elem ) != expected ) |
||||
find_child( ".collapser", sr_elem ).click() |
||||
wait_for( 2, lambda: _is_collapsed( sr_elem ) == expected ) |
||||
|
||||
# check if there will be an entry in the rule info popup |
||||
if not ruleid: |
||||
return |
||||
|
||||
# yup - bring that entry up and check it |
||||
results = do_search( ruleid ) |
||||
popup = wait_for_elem( 2, "#rule-info" ) |
||||
elems = find_children( ".rule-info", popup ) |
||||
assert len(elems) == 1 |
||||
elem = elems[0] |
||||
assert _is_collapsed( elem ) == expected |
||||
|
||||
# toggle the state and check the results |
||||
if expected is not None: |
||||
find_child( ".collapser", elem ).click() |
||||
wait_for( 2, lambda: _is_collapsed( elem ) != expected ) |
||||
find_child( ".collapser", elem ).click() |
||||
wait_for( 2, lambda: _is_collapsed( elem ) == expected ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_asop_preamble( webapp, webdriver ): |
||||
"""Test ASOP preambles.""" |
||||
|
||||
# initialize |
||||
webapp.control_tests.set_data_dir( "collapsible" ) |
||||
init_webapp( webapp, webdriver ) |
||||
select_tabbed_page( "nav", "asop" ) |
||||
nav = unload_asop_nav( True ) |
||||
|
||||
def check_preamble( expected ): |
||||
# check if the preamble is collapsed/expanded |
||||
elem = find_child( "#asop" ) |
||||
assert _is_collapsed( elem, has_collapsible=True ) == expected |
||||
|
||||
# open the "no content" section |
||||
open_asop_chapter( "no-content", nav ) |
||||
check_preamble( None ) |
||||
|
||||
# open the "short content" section |
||||
open_asop_chapter( "short-content", nav ) |
||||
check_preamble( None ) |
||||
|
||||
# open the "long content" section |
||||
open_asop_chapter( "long-content", nav ) |
||||
check_preamble( False ) |
||||
|
||||
# collapse the preamble |
||||
find_child( "#asop .collapser" ).click() |
||||
wait_for( 2, lambda: _is_collapsed( find_child("#asop") ) ) |
||||
# nb: the preamble is now collapsed |
||||
|
||||
# open the "short content" section |
||||
open_asop_chapter( "short-content", nav ) |
||||
check_preamble( None ) |
||||
|
||||
# check that the preamble is still collapsed in the "long content" section |
||||
open_asop_chapter( "long-content", nav ) |
||||
check_preamble( True ) |
||||
|
||||
# switch to another nav pane, then check that the preamble is still collapsed when we come back |
||||
open_asop_chapter( "short-content", nav ) |
||||
select_tabbed_page( "nav", "search" ) |
||||
select_tabbed_page( "nav", "asop" ) |
||||
check_preamble( None ) |
||||
open_asop_chapter( "long-content", nav ) |
||||
check_preamble( True ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def _is_collapsed( elem, has_collapsible=True ): |
||||
"""Check the state of a collapser and its associated collapsible.""" |
||||
|
||||
# get the state of the collapser |
||||
collapser = find_child( "img.collapser", elem ) |
||||
if not collapser: |
||||
return None |
||||
url = collapser.get_attribute( "src" ) |
||||
if url.endswith( "collapser-down.png" ): |
||||
is_collapsed = True |
||||
else: |
||||
assert url.endswith( "collapser-up.png" ) |
||||
is_collapsed = False |
||||
|
||||
# check the state of the associated collapsible |
||||
collapsible = find_child( ".collapsible", elem ) |
||||
if has_collapsible: |
||||
if is_collapsed: |
||||
assert has_class( collapsible, "collapsed" ) |
||||
else: |
||||
assert not has_class( collapsible, "collapsed" ) |
||||
else: |
||||
assert collapsible is None |
||||
|
||||
return is_collapsed |
Loading…
Reference in new issue