diff --git a/evennia/web/webclient/static/webclient/css/goldenlayout.css b/evennia/web/webclient/static/webclient/css/goldenlayout.css new file mode 100644 index 0000000000..54b5663e8a --- /dev/null +++ b/evennia/web/webclient/static/webclient/css/goldenlayout.css @@ -0,0 +1,106 @@ +/* + * + */ + +#clientwrapper #main { + height: 100%; +} + +#clientwrapper #main #main-sub { + height: 100%; +} + +#toolbar { + display: none; +} + +#optionsbutton { + font-size: .9rem; + width: .9rem; + vertical-align: top; +} + +.content { + height: 100%; + overflow-y: auto; + overflow-x: hidden; +} + +body .lm_popout { + display: none; +} + +label { + display: block; +} + +.lm_title { + text-align: center; +} + +#typelist { + position: relative; +} + +.typelistsub { + position: absolute; + top: 5px; + left: 5px; + width: max-content; + background-color: #333333; + line-height: .5em; + padding: .3em; + font-size: .9em; + z-index: 1; +} + +#renamebox { + position: relative; +} + +#renameboxin { + position: absolute; + z-index: 1; +} + +#updatelist { + position: relative; +} + +.updatelistsub { + position: absolute; + top: 5px; + left: 5px; + width: max-content; + background-color: #333333; + line-height: .5em; + padding: .3em; + font-size: .9em; + z-index: 1; +} + +#inputcontrol { + height: 100%; +} + +.inputwrap { + position: relative; + height: 100%; +} + +#inputsend { + position: absolute; + right: 0; + z-index: 1; +} + +#inputfield { + position: absolute; + top: 0; +} + +.glbutton { + font-size: 0.6em; + line-height: 1.3em; + padding: .5em; +} diff --git a/evennia/web/webclient/static/webclient/js/plugins/default_in.js b/evennia/web/webclient/static/webclient/js/plugins/default_in.js index 02fd401706..28bfc9f315 100644 --- a/evennia/web/webclient/static/webclient/js/plugins/default_in.js +++ b/evennia/web/webclient/static/webclient/js/plugins/default_in.js @@ -8,7 +8,6 @@ let defaultin_plugin = (function () { // // handle the default key triggering onSend() var onKeydown = function (event) { - $("#inputfield").focus(); if ( (event.which === 13) && (!event.shiftKey) ) { // Enter Key without shift var inputfield = $("#inputfield"); var outtext = inputfield.val(); diff --git a/evennia/web/webclient/static/webclient/js/plugins/goldenlayout.js b/evennia/web/webclient/static/webclient/js/plugins/goldenlayout.js new file mode 100644 index 0000000000..f0aa47b67c --- /dev/null +++ b/evennia/web/webclient/static/webclient/js/plugins/goldenlayout.js @@ -0,0 +1,403 @@ +/* + * + * Golden Layout plugin + * + */ +plugin_handler.add('goldenlayout', (function () { + + var myLayout; + var input_component = null; + var known_types = ['all', 'untagged']; + var untagged = []; + + var config = { + content: [{ + type: 'column', + content: [{ + type: 'component', + componentName: 'Main', + isClosable: false, + componentState: { + types: 'untagged', + update_method: 'newlines', + } + }, { + type: 'component', + componentName: 'input', + id: 'inputComponent', + height: 15, + isClosable: false, + }] + }] + }; + + + var newDragConfig = { + title: 'Untitled', + type: 'component', + componentName: 'evennia', + componentState: { + types: 'all', + update_method: 'newlines', + }, + }; + + + // helper function: filter vals out of array + function filter (vals, array) { + let tmp = array.slice(); + for (i=0; i -1 ) { + tmp.splice( tmp.indexOf(val), 1 ); + } + } + return tmp; + } + + + // + // Calculate all known_types minus the 'all' type, + // then filter out all types that have been mapped to a pane. + var calculate_untagged_types = function () { + // set initial untagged list + untagged = filter( ['all', 'untagged'], known_types); + // for each .content pane + $('.content').each( function () { + let types = $(this).attr('types'); + if ( typeof types !== "undefined" ) { + untagged = filter( types.split(' '), untagged ); + } + }); + } + + + // Save the GoldenLayout state to localstorage whenever it changes. + var onStateChanged = function () { + let components = myLayout.root.getItemsByType('component'); + components.forEach( function (component) { + if( component.hasId('inputComponent') ) { return; } // ignore input components + + let text_div = component.container.getElement().children('.content'); + let types = text_div.attr('types'); + let update_method = text_div.attr('update_method'); + component.container.extendState({ 'types': types, 'update_method': update_method }); + }); + + var state = JSON.stringify( myLayout.toConfig() ); + localStorage.setItem( 'evenniaGoldenLayoutSavedState', state ); + } + + + // + // + // Handle the renamePopup + var renamePopup = function (evnt) { + let element = $(evnt.data.contentItem.element); + let content = element.find('.content'); + let title = evnt.data.contentItem.config.title; + let renamebox = document.getElementById('renamebox'); + if( !renamebox ) { + renamebox = $('
'); + renamebox.append(''); + renamebox.insertBefore( content ); + } else { + let title = $('#renameboxin').val(); + evnt.data.setTitle( title ); + evnt.data.contentItem.setTitle( title ); + myLayout.emit('stateChanged'); + $('#renamebox').remove(); + } + } + + + // + var onSelectTypesClicked = function (evnt) { + let element = $(evnt.data.contentItem.element); + let content = element.find('.content'); + let selected_types = content.attr('types'); + let menu = $('
'); + let div = $('
'); + + if( selected_types ) { + selected_types = selected_types.split(' '); + } + for (i=0; i'+type+''); + } else { + choice = $(''); + } + choice.appendTo(div); + } + div.appendTo(menu); + + element.prepend(menu); + } + + + // + // + var commitCheckboxes = function (evnt) { + let element = $(evnt.data.contentItem.element); + let content = element.find('.content'); + let checkboxes = $('#typelist :input'); + let types = []; + for (i=0; i'); + let div = $('
'); + + let newlines = $(''); + let append = $(''); + let replace = $(''); + + newlines.appendTo(div); + append.appendTo(div); + replace.appendTo(div); + + div.appendTo(menu); + + element.prepend(menu); + } + + + // + // Handle the updatePopup + var updatePopup = function (evnt) { + let updatelist = document.getElementById('updatelist'); + if( !updatelist ) { + onUpdateMethodClicked(evnt); + } else { + let element = $(evnt.data.contentItem.element); + let content = element.find('.content'); + content.attr('update_method', $('input[name=update_method]:checked').val() ); + myLayout.emit('stateChanged'); + $('#updatelist').remove(); + } + } + + + // + // + var onTabCreate = function (tab) { + //HTML for the typeDropdown + let tabRenameControl = $('\u2B57'); + let typePopupControl = $(''); + let updatePopupControl = $(''); + let splitControl = $('+'); + + // track popups when the associated control is clicked + tabRenameControl.click( tab, renamePopup ); + + typePopupControl.click( tab, typePopup ); + + updatePopupControl.click( tab, updatePopup ); + + splitControl.click( tab, function (evnt) { + evnt.data.header.parent.addChild( newDragConfig ); + }); + + // Add the typeDropdown to the header + tab.element.prepend( tabRenameControl ); + tab.element.append( typePopupControl ); + tab.element.append( updatePopupControl ); + tab.element.append( splitControl ); + + if( tab.contentItem.config.componentName == "Main" ) { + tab.element.prepend( $('#optionsbutton').clone(true).addClass('lm_title') ); + } + } + + + // + // + var scrollAll = function () { + let components = myLayout.root.getItemsByType('component'); + components.forEach( function (component) { + if( component.hasId('inputComponent') ) { return; } // ignore input components + + let text_div = component.container.getElement().children('.content'); + let scrollHeight = text_div.prop('scrollHeight'); + let clientHeight = text_div.prop('clientHeight'); + text_div.scrollTop( scrollHeight - clientHeight ); + }); + myLayout.updateSize(); + } + + + // + // + var route_msg = function (text_div, txt, update_method) { + if ( update_method == 'replace' ) { + text_div.html(txt) + } else if ( update_method == 'append' ) { + text_div.append(txt); + } else { // line feed + text_div.append('
' + txt + '
'); + } + let scrollHeight = text_div.prop('scrollHeight'); + let clientHeight = text_div.prop('clientHeight'); + text_div.scrollTop( scrollHeight - clientHeight ); + } + + + // + // Public + // + + + // + // + var initComponent = function (div, container, state, default_types, update_method) { + // set this container's content div types attribute + if( state ) { + div.attr('types', state.types); + div.attr('update_method', state.update_method); + } else { + div.attr('types', default_types); + div.attr('update_method', update_method); + } + div.appendTo( container.getElement() ); + container.on('tab', onTabCreate); + } + + + // + // + var onText = function (args, kwargs) { + // If the message is not itself tagged, we'll assume it + // should go into any panes with 'all' and 'untagged' set + var msgtype = 'untagged'; + + if ( kwargs && 'type' in kwargs ) { + msgtype = kwargs['type']; + if ( ! known_types.includes(msgtype) ) { + // this is a new output type that can be mapped to panes + console.log('detected new output type: ' + msgtype) + known_types.push(msgtype); + untagged.push(msgtype); + } + } + + let message_delivered = false; + let components = myLayout.root.getItemsByType('component'); + + components.forEach( function (component) { + if( component.hasId('inputComponent') ) { return; } // ignore the input component + + let text_div = component.container.getElement().children('.content'); + let attr_types = text_div.attr('types'); + let pane_types = attr_types ? attr_types.split(' ') : []; + let update_method = text_div.attr('update_method'); + let txt = args[0]; + + // is this message type listed in this pane's types (or is this pane catching 'all') + if( pane_types.includes(msgtype) || pane_types.includes('all') ) { + route_msg( text_div, txt, update_method ); + message_delivered = true; + } + + // is this pane catching 'upmapped' messages? + // And is this message type listed in the untagged types array? + if( pane_types.includes("untagged") && untagged.includes(msgtype) ) { + route_msg( text_div, txt, update_method ); + message_delivered = true; + } + }); + + if ( message_delivered ) { + return true; + } + // unhandled message + return false; + } + + + // + // required Init me + var init = function (options) { + // Set up our GoldenLayout instance built off of the default main-sub div + var savedState = localStorage.getItem( 'evenniaGoldenLayoutSavedState' ); + var mainsub = document.getElementById('main-sub'); + + if( savedState !== null ) { + myLayout = new GoldenLayout( JSON.parse( savedState ), mainsub ); + } else { + myLayout = new GoldenLayout( config, mainsub ); + } + + // register our component and replace the default messagewindow with the Main component element + myLayout.registerComponent( 'Main', function (container, componentState) { + let main = $('#messagewindow').addClass('content'); + initComponent(main, container, componentState, 'untagged', 'newlines' ); + }); + + myLayout.registerComponent( 'input', function (container, componentState) { + $('#inputcontrol').remove(); // remove the cluttered, HTML-defined input divs + $('
') + .append( '
' ) + .append( '' ) + .appendTo( container.getElement() ); + }); + + myLayout.registerComponent( 'evennia', function (container, componentState) { + let div = $('
'); + initComponent(div, container, componentState, 'all', 'newlines'); + container.on('destroy', calculate_untagged_types); + }); + + // Make it go. + myLayout.init(); + + // Event when client window changes + $(window).bind("resize", scrollAll); + + // Set Save State callback + myLayout.on( 'stateChanged', onStateChanged ); + + console.log('Golden Layout Plugin Initialized.'); + } + + return { + init: init, + onText: onText, + getGL: function () { return myLayout }, + initComponent: initComponent, + } +})()); diff --git a/evennia/web/webclient/templates/webclient/base.html b/evennia/web/webclient/templates/webclient/base.html index 0cc4302af4..bb88525cca 100644 --- a/evennia/web/webclient/templates/webclient/base.html +++ b/evennia/web/webclient/templates/webclient/base.html @@ -17,7 +17,6 @@ JQuery available. - @@ -64,8 +63,14 @@ JQuery available. + + + + + {% block guilib_import %} @@ -73,10 +78,13 @@ JQuery available. + + {% endblock %}