diff --git a/frontend/www/index.html b/frontend/www/index.html index f6e3eaa4..bebd6b20 100644 --- a/frontend/www/index.html +++ b/frontend/www/index.html @@ -325,6 +325,10 @@ src="./src/js/modules/dashboard/relationmapper/relationMapper.js" type="application/javascript" > + diff --git a/frontend/www/src/css/dashboard-stemmaweb.css b/frontend/www/src/css/dashboard-stemmaweb.css index 381f69e8..624c7242 100644 --- a/frontend/www/src/css/dashboard-stemmaweb.css +++ b/frontend/www/src/css/dashboard-stemmaweb.css @@ -29,6 +29,16 @@ padding: 0% 1.5% 0% 1.5%; } +#main.col-7 { + transition: width 1s, margin 1s; +} + +#main.col-9 { + margin-left: 0; + transition: width 1s, margin 1s; + transition-delay: 500ms, 500ms; +} + /* * Sidebar */ @@ -103,6 +113,10 @@ edit-stemma-buttons { align-items: flex-start; } +#edit-stemma-buttons-right .greyed-out { + pointer-events: none; +} + #stemma-selector-buttons { width: 50%; display: flex; @@ -258,12 +272,12 @@ edit-stemma-buttons a.greyed-out div { transition: width 0.4s ease-in-out; } -#stemma-selectors { +#stemma-selectors, #section-selectors { display: flex; justify-content: flex-start; } -.stemma-selector.selected svg { +.stemma-selector.selected svg, .section-selector.selected svg { fill: rgb(210,210,210); } @@ -422,4 +436,16 @@ relation-mapper { button.selected-view { color: #fff; background-color: #87aade; +} + +#section-title { + font-size: 140%; +} + +#section-loader-spinner { + display: none; +} + +#section-loader-spinner.show { + display: inline; } \ No newline at end of file diff --git a/frontend/www/src/js/modules/common/effects.js b/frontend/www/src/js/modules/common/effects.js index 842a7668..42aa6bee 100644 --- a/frontend/www/src/js/modules/common/effects.js +++ b/frontend/www/src/js/modules/common/effects.js @@ -19,30 +19,70 @@ function speedy_transition(transition) { return transition.delay( 25 ).duration( 150 ).ease( d3.easeLinear ); } -function crossFade( elementIn, elementOut=null, options={ 'display': 'flex', 'duration': 1000 } ){ +function crossFade( elementIn, elementOut=null, options={} ){ + const defaults = { 'display': 'flex', 'duration': 1000, 'onEnd': null }; + const usedOptions = { ...defaults, ...options }; if( elementOut ) { var elemIn = d3.select( elementIn ); var elemOut = d3.select( elementOut ); - var duration = options.duration / 2; + var stepDuration = usedOptions.duration / 2; elemOut .transition() - .duration( duration ) - .style( 'opacity', 0) + .duration( stepDuration ) + .style( 'opacity', 0 ) .on( 'end', () => { elemOut.style( 'display', 'none' ); elemIn - .style( 'display', options.display ) + .style( 'display', usedOptions.display ) .transition() - .duration( duration ) - .style( 'opacity', 1 ); + .duration( stepDuration ) + .style( 'opacity', 1 ) + .on( 'end', usedOptions.onEnd ); }) } } -function fadeToDisplayFlex( element ){ +function fadeToDisplayFlex( element, options ){ + const defaultOptions = { 'duration': 500, 'delay': 0, 'onEnd': null }; + const usedOptions = { ...defaultOptions, ...options }; d3.select( element ) .style( 'display', 'flex' ) .transition() - .duration( 1000 ) - .style( 'opacity', 1 ); + .duration( usedOptions.duration ) + .style( 'opacity', 1 ) + .on( 'end', () => { + if ( usedOptions.onEnd ) { + usedOptions.onEnd(); + } + } ); +} + +function fadeToDisplayNone( element, options={} ){ + const defaultOptions = { 'duration': 500, 'delay': 0, 'reverse': false, 'onEnd': null }; + const usedOptions = { ...defaultOptions, ...options }; + if( !options.reverse ) { + d3.select( element ) + .transition() + .delay( usedOptions.delay ) + .duration( usedOptions.duration ) + .style( 'opacity', 0 ) + .on( 'end', () => { + d3.select( element ).style( 'display', 'none' ); + if( usedOptions.onEnd ){ + usedOptions.onEnd(); + } + } ); + } else { + d3.select( '#sidebar-menu' ).node().style.removeProperty( 'display' ) + d3.select( element ) + .transition() + .delay( usedOptions.delay ) + .duration( usedOptions.duration ) + .style( 'opacity', 1 ) + .on( 'end' , () => { + if( usedOptions.onEnd ) { + usedOptions.onEnd(); + } + } ); + } } diff --git a/frontend/www/src/js/modules/dashboard/relationmapper/relationMapper.js b/frontend/www/src/js/modules/dashboard/relationmapper/relationMapper.js index 78deb3ad..0c13b72f 100644 --- a/frontend/www/src/js/modules/dashboard/relationmapper/relationMapper.js +++ b/frontend/www/src/js/modules/dashboard/relationmapper/relationMapper.js @@ -20,7 +20,10 @@ class RelationMapper extends HTMLElement { } render() { - this.innerHTML = `
` + this.innerHTML = ` +
+ +
` } } diff --git a/frontend/www/src/js/modules/dashboard/relationmapper/relationRenderer.js b/frontend/www/src/js/modules/dashboard/relationmapper/relationRenderer.js index 8643b2b1..dbdf608f 100644 --- a/frontend/www/src/js/modules/dashboard/relationmapper/relationRenderer.js +++ b/frontend/www/src/js/modules/dashboard/relationmapper/relationRenderer.js @@ -1,10 +1,20 @@ class RelationRenderer { #relGvr = null; - + #height = 0; + #width = 0; + constructor() { } + set height( height ) { + this.#height = height; + } + + set width( width ) { + this.#width = width; + } + get relationMapperGraphvizRoot() { if( this.#relGvr == null ){ this.#createGraphvizRoot(); @@ -23,14 +33,10 @@ class RelationRenderer { const graph = selection.empty() ? relationMapperArea.append( 'div' ).attr( 'id', 'relation-graph' ) : selection; - // Because the relation mapper container is `display: none` on initialization - // we use the dimensions of the stemma renderer that is already depicted. - const stemmaRendererDimensions = document.querySelector( '#graph' ).getBoundingClientRect(); - graph.style( 'height', `${stemmaRendererDimensions.height}px` ); + graph.style( 'height', `${this.#height}px` ); this.#relGvr = graph .graphviz() - .width( stemmaRendererDimensions.width ) - .height( stemmaRendererDimensions.height ); + .logEvents( false ); } /** @@ -40,44 +46,72 @@ class RelationRenderer { * @param {Tradition} tradition * @param {Stemma} stemma */ - renderRelationsGraph( dot ) { + renderRelationsGraph( dot, options={} ) { + const defaultOptions = { + 'onEnd': () => {} + }; + const usedOptions = { ...defaultOptions, ...options }; + this.#height = usedOptions.height || this.#height; + this.#width = usedOptions.width || this.#width; + this.relationMapperGraphvizRoot + .width( this.#width ) + .height( this.#height ) + .on( 'end', usedOptions.onEnd ); this.relationMapperGraphvizRoot.renderDot( dot ); if( this.relationMapperGraphvizRoot.zoomSelection() != null ){ this.relationMapperGraphvizRoot.resetZoom(); }; } - // /** - // * Resizes the current graph/stemma when the browser window gets - // * resized. Also set the new corresponding with on the GraphViz - // * renderer so that subsequent stemmas are depicted at the right - // * size. - // */ - // resizeSVG() { - // const margin = 14; - // const stemmaButtonsRowHeight = document.querySelector( '#stemma-buttons' ).getBoundingClientRect()['height']; - // const bbrect = document.querySelector( '#graph-area' ).getBoundingClientRect(); - // const width = bbrect['width'] - ( 2 * margin ); - // const factor = bbrect['height'] / window.innerHeight; - // const height = bbrect['height'] - stemmaButtonsRowHeight; - // const graphArea = d3.select('#graph-area'); - // const svg = graphArea.select("#graph").selectWithoutDataPropagation("svg"); - // svg - // .transition() - // .duration(700) - // .attr("width", width ) - // .attr("height", height ); - // // This is a bit weird, but we need to reset the size of the original - // // graphviz renderer that was set when the line - // // `const stemmaRenderer = new StemmaRenderer();` - // // was executed, and not on `this`. There's probably - // // cleaner ways to do this. - // stemmaRenderer.graphvizRoot.width( width ); - // stemmaRenderer.graphvizRoot.height( height ); - // } - + // TODO: resizing on window change size. + + /** + // TODO(?): Why do we destroy the graphviz instance for the relation mapper on the node + * on the node we created it for? It makes more sense to keep the instance and reuse + * it to depict new versions of the same relation graph, or to depict relations + * form other sections/traditions, right? Yes, except if we do the rendering of + * subsequent relation graphs takes forever. Below are the logs of an initial and + * follow up renderings (numbers are time in ms per event). No idea why the same + * graph takes 7 seconds to render a second time, while it only takes 1 initially. + * + * Initial rendering + * + * Event 2 layoutStart 0 + * Event 3 layoutEnd 869 + * Event 4 dataExtractEnd 81 + * Event 5 dataProcessPass1End 19 + * Event 6 dataProcessPass2End 5 + * Event 7 dataProcessEnd 1 + * Event 8 renderStart 0 + * Event 14 zoom 119 + * Event 9 renderEnd 0 + * Event 13 end 0 + * + * + * Second rendering + * + * Event 2 layoutStart 1 + * Event 3 layoutEnd 847 + * Event 4 dataExtractEnd 73 + * Event 5 dataProcessPass1End 6259 + * Event 6 dataProcessPass2End 5 + * Event 7 dataProcessEnd 0 + * Event 8 renderStart 0 + * Event 14 zoom 1 + * Event 9 renderEnd 83 + * Event 13 end 1 + * Event 14 zoom 0 + */ + + destroy() { + if ( this.#relGvr ) { + this.#relGvr.destroy(); + this.#relGvr = null; + } + d3.select( '#relation-graph' ).remove(); + } + } const relationRenderer = new RelationRenderer(); -// d3.select( window ).on( 'resize', stemmaRenderer.resizeSVG ); \ No newline at end of file diff --git a/frontend/www/src/js/modules/dashboard/relationmapper/sectionSelectors.js b/frontend/www/src/js/modules/dashboard/relationmapper/sectionSelectors.js new file mode 100644 index 00000000..4ce1c40f --- /dev/null +++ b/frontend/www/src/js/modules/dashboard/relationmapper/sectionSelectors.js @@ -0,0 +1,90 @@ +/** + * Object to interact with the Stemmarest Middleware's API through high-level + * functions. + * + * @type {StemmarestService} + */ +const sectionSelectorsService = stemmarestService; + +class SectionSelectors extends HTMLElement { + constructor() { + super(); + } + + connectedCallback() { + this.render(); + SECTION_STORE.subscribe( this.onSectionStateChanged ); + } + + /** + * This function will be called each time the state persisted in the + * `SECTION_STORE` changes. It will update the UI to reflect the current + * state. + * + * @param {SectionState} state + */ + onSectionStateChanged( prevState, state ) { + // First off, we don't need to do anything if we're not visibleā€¦ + if ( window.getComputedStyle( document.querySelector( 'relation-mapper' ) ).display != 'none' ) { + // We only do something if the section really changed. + if ( state.selectedSection != prevState.selectedSection ) { + sectionSelectorsService.getSectionDot( TRADITION_STORE.state.selectedTradition.id, state.selectedSection.id ).then( (resp) => { + if ( resp.success ) { + const graphArea = d3.select('#relation-graph'); + document.querySelector( '#section-title' ).innerHTML = `${SECTION_STORE.state.selectedSection.name}`; + graphArea.transition().call( speedy_transition ).style( 'opacity', '0.0' ).on( 'end', () => { + relationRenderer.renderRelationsGraph( + resp.data, { + 'onEnd': () => { graphArea.transition().call( mellow_transition ).style('opacity', '1.0' ); } + } + ); + } ); + SectionSelectors.renderSectionSelectors(); + } else { + StemmawebAlert.show( + `Could not fetch section graph information: ${resp.message}`, + 'danger' + ); + } + } ); + } + } + } + + static renderSectionSelectors() { + const sections = SECTION_STORE.state.availableSections; + // Here we put in the slide indicators that will allow the user to + // switch to different sections in the relation mapper. + const sectionSelector = d3.select('#section-selectors'); + sectionSelector.selectAll('*').remove(); + sectionSelector + .selectAll( 'span' ) + .data( sections ) + .enter() + .append( 'span' ) + .html( (d, i) => { + const selectedIndex = SECTION_STORE.selectedIndex; + const isSelected = + (selectedIndex === -1 && i === 0) || selectedIndex === i; + const selectedAttr = isSelected + ? " selected" + : ""; + return ``; + }) + .on( 'click', function (e, d) { + // Update the state with the selected section + SECTION_STORE.setSelectedSection( d ); + } ); + } + + render() { + this.innerHTML = ` +
+
+
+
+ `; + } +} + +customElements.define( 'section-selectors', SectionSelectors ); \ No newline at end of file diff --git a/frontend/www/src/js/modules/dashboard/tradition/section/state.js b/frontend/www/src/js/modules/dashboard/tradition/section/state.js index a46acc24..7d5408d4 100644 --- a/frontend/www/src/js/modules/dashboard/tradition/section/state.js +++ b/frontend/www/src/js/modules/dashboard/tradition/section/state.js @@ -37,6 +37,16 @@ class SectionStore extends StateStore { }); } + /** + * @returns {number} The index of the currently selected section in the list of + * available sections for the selected tradition. + */ + get selectedIndex() { + const { availableSections, selectedSection } = this.state; + return availableSections.indexOf( selectedSection ); + } + + /** * Informs all SectionLists (well, at least those that registered * a listener for the event) that a section was added. diff --git a/frontend/www/src/js/modules/dashboard/tradition/stemma/stemmaButtons.js b/frontend/www/src/js/modules/dashboard/tradition/stemma/stemmaButtons.js index 96fcd3ad..a76c1c83 100644 --- a/frontend/www/src/js/modules/dashboard/tradition/stemma/stemmaButtons.js +++ b/frontend/www/src/js/modules/dashboard/tradition/stemma/stemmaButtons.js @@ -19,52 +19,105 @@ class StemmaButtons extends HTMLElement { * needs to be shown and what button needs to be highlighted and * un-highligthed. */ - - //TODO (PRIO): the toggles shouldn't fire if that view is already active! - - document.querySelector( '#view-stemmata-button' ).addEventListener( 'click', ( evt ) => { - this.toggleViewButton( evt ); - this.toggleDisplayRelationMapper(); - } ); - document.querySelector( '#delete-tradition-button' ).addEventListener( 'click', this.handleDelete ); + + document.querySelector( '#view-stemmata-button' ).addEventListener( 'click', this.setView ); document.querySelector( '#run-stemweb-button' ).addEventListener( 'click', stemwebFrontend.showDialog ); - document.querySelector( '#edit-collation-button' ).addEventListener( 'click', ( evt ) => { - this.toggleViewButton( evt ); - this.toggleDisplayRelationMapper(); - } ); + document.querySelector( '#edit-collation-button' ).addEventListener( 'click', this.setView ); + document.querySelector( '#delete-tradition-button' ).addEventListener( 'click', this.handleDelete ); + + SECTION_STORE.subscribe( this.toggleEditCollationButtonActive ); + fadeIn( this ); } - toggleViewButton( evt ) { - document.querySelectorAll( '#view-selectors button' ).forEach( ( elem ) => { - elem == evt.currentTarget ? elem.classList.add( 'selected-view' ) : elem.classList.remove( 'selected-view' ); - } ); + /** + * This takes care of the edit collation button to be greyed out + * if there is no section selected. + */ + toggleEditCollationButtonActive() { + const editCollationButtonElement = document.querySelector( '#edit-collation-button' ); + if( SECTION_STORE.state.selectedSection ) { + if ( editCollationButtonElement.classList.contains( 'disabled' ) ) { + editCollationButtonElement.classList.remove( 'disabled' ); + } + } else { + if ( !editCollationButtonElement.classList.contains( 'disabled' ) ) { + editCollationButtonElement.classList.add( 'disabled' ); + } + } } - - toggleDisplayRelationMapper() { - const relationMapperElement = document.querySelector( 'relation-mapper' ); - const stemmaEditorGraphContainerElement = document.querySelector( '#stemma-editor-graph-container' ); - if ( window.getComputedStyle( document.querySelector( 'relation-mapper' ) ).display == "none" ) { - crossFade( relationMapperElement, stemmaEditorGraphContainerElement ); - const section = SECTION_STORE.state.selectedSection; - if( section ) { - stemmaButtonsService.getSectionDot( TRADITION_STORE.state.selectedTradition.id, section.id ).then((resp) => { - if ( resp.success ) { - relationRenderer.renderRelationsGraph( resp.data ) ; - } else { - StemmawebAlert.show( - `Could not fetch section graph information: ${resp.message}`, - 'danger' - ); - } - } ); + setView( evt ) { + const currentView = document.querySelector( '#view-selectors .selected-view ' ); + var targetView = null; + var fadeOutElement = null; + if ( !( evt.currentTarget == currentView ) ) { + // Set the right button to highlight. + currentView.classList.remove( 'selected-view' ); + evt.currentTarget.classList.add( 'selected-view' ); + // Figure out the chosen view (targetView) and do what needs to happen to prepare it. + if ( evt.currentTarget == document.querySelector( '#view-stemmata-button' ) ) { + targetView = document.querySelector( '#stemma-editor-graph-container' ); + } + if ( evt.currentTarget == document.querySelector( '#edit-collation-button' ) ) { + targetView = document.querySelector( 'relation-mapper' ); + var section = SECTION_STORE.state.selectedSection; + if ( !section ) { + section = SECTION_STORE.state.availableSections[0]; + SECTION_STORE.setSelectedSection( section ); + } + if( section ) { + stemmaButtonsService.getSectionDot( TRADITION_STORE.state.selectedTradition.id, section.id ).then( (resp) => { + if ( resp.success ) { + // Because the relation mapper container is `display: none` on initialization we use the height of other elements. + // Timing is relevant: closeStemmaView takes a callback and when that gets executed the stemma graph container + // is already `display: none` and `getBoundingClientRect()` return just zero on any dimension. + const graphRendererHeight = document.querySelector( '#graph-area' ).getBoundingClientRect().height; + SectionSelectors.renderSectionSelectors(); + StemmaButtons.closeStemmaView( () => { + const graphRendererWidth = document.querySelector( '#topbar-menu' ).getBoundingClientRect().width; + relationRenderer.renderRelationsGraph( + resp.data, { + 'width': graphRendererWidth, + 'height': graphRendererHeight, + 'onEnd': () => { + fadeToDisplayFlex( targetView, { 'duration': 1500 } ); + document.querySelector( '#section-title' ).innerHTML = `${SECTION_STORE.state.selectedSection.name}`; + } + } + ); + } ); + } else { + StemmawebAlert.show( + `Could not fetch section graph information: ${resp.message}`, + 'danger' + ); + } + } ); + } + } + // Figure out which view we are closing, set that as element to + // fade out, and remove or stash stuff from the view we are closing. + if ( currentView == document.querySelector( '#edit-collation-button' ) ) { + relationRenderer.destroy(); // Wondering? See the elaborate note in relationRenderer.js. + document.querySelector( '#section-title' ).innerHTML = ''; + fadeOutElement = document.querySelector( 'relation-mapper' ); + document.querySelector( '#main' ).classList.remove( 'col-9' ); + document.querySelector( '#main' ).classList.add( 'col-7' ); + fadeToDisplayNone( '#sidebar-menu', { 'reverse': true, 'delay': 500 } ); + crossFade( targetView, fadeOutElement ); } - } else { - crossFade( stemmaEditorGraphContainerElement, relationMapperElement ); } } + static closeStemmaView( callBack ) { + const fadeOutElement = document.querySelector( '#stemma-editor-graph-container' ); + fadeToDisplayNone( '#sidebar-menu', { 'delay': 0 } ); + document.querySelector( '#main' ).classList.remove( 'col-7' ); + document.querySelector( '#main' ).classList.add( 'col-9' ); // Timed in CSS to 1s with 500ms delay, hence duration of 1500 in next line. + fadeToDisplayNone( fadeOutElement, { 'duration': 1500, 'onEnd': callBack } ); + } + handleDelete() { const { selectedTradition: tradition, availableTraditions } = TRADITION_STORE.state; @@ -115,10 +168,10 @@ class StemmaButtons extends HTMLElement { - - diff --git a/frontend/www/src/js/modules/dashboard/tradition/traditionTitle.js b/frontend/www/src/js/modules/dashboard/tradition/traditionTitle.js index 8d4e4ea9..64aef862 100644 --- a/frontend/www/src/js/modules/dashboard/tradition/traditionTitle.js +++ b/frontend/www/src/js/modules/dashboard/tradition/traditionTitle.js @@ -34,13 +34,27 @@ class TraditionTitle extends HTMLElement { }); } + static set altTitle( title ) { + TraditionTitle.render( title ); + } + connectedCallback() { this.render(); fadeIn( this ); } + setTitle( title ) { + this.#title = title; + this.render(); + } + render() { - this.innerHTML = `

${this.#title}

` + this.innerHTML = `
+

+ ${this.#title} +

+
+ `; } } diff --git a/frontend/www/src/js/modules/dashboard/tradition/traditionView.js b/frontend/www/src/js/modules/dashboard/tradition/traditionView.js index 206152ee..32f21ba0 100644 --- a/frontend/www/src/js/modules/dashboard/tradition/traditionView.js +++ b/frontend/www/src/js/modules/dashboard/tradition/traditionView.js @@ -30,8 +30,10 @@ class TraditionView extends HTMLElement { } ); } ) .catch( (error) => { - // TODO: some generic error handling? - console.log(error); + StemmawebAlert.show( + `Error during rooting of stemma: ${res.message}`, + 'danger' + ); } ); } @@ -96,9 +98,10 @@ class TraditionView extends HTMLElement { render() { this.innerHTML = ` -
+
-
+
+