From aa19b9b73f1197c30999172beaf99aadce9d03c1 Mon Sep 17 00:00:00 2001 From: Brenden Tuck Date: Mon, 27 Aug 2018 00:24:27 -0400 Subject: [PATCH] Plugin-ify the webclient --- .../static/webclient/css/webclient.css | 7 + .../static/webclient/js/plugins/default_in.js | 44 + .../webclient/js/plugins/default_out.js | 60 ++ .../webclient/js/plugins/default_unload.js | 17 + .../static/webclient/js/plugins/history.js | 116 +++ .../webclient/js/plugins/notifications.js | 88 ++ .../static/webclient/js/plugins/oob.js | 35 + .../static/webclient/js/plugins/options.js | 162 ++++ .../static/webclient/js/plugins/popups.js | 101 +++ .../webclient/js/plugins/splithandler.js | 368 +++++++++ .../static/webclient/js/splithandler.js | 145 ---- .../static/webclient/js/webclient_gui.js | 774 +++++------------- .../webclient/templates/webclient/base.html | 10 +- .../templates/webclient/webclient.html | 62 +- 14 files changed, 1230 insertions(+), 759 deletions(-) create mode 100644 evennia/web/webclient/static/webclient/js/plugins/default_in.js create mode 100644 evennia/web/webclient/static/webclient/js/plugins/default_out.js create mode 100644 evennia/web/webclient/static/webclient/js/plugins/default_unload.js create mode 100644 evennia/web/webclient/static/webclient/js/plugins/history.js create mode 100644 evennia/web/webclient/static/webclient/js/plugins/notifications.js create mode 100644 evennia/web/webclient/static/webclient/js/plugins/oob.js create mode 100644 evennia/web/webclient/static/webclient/js/plugins/options.js create mode 100644 evennia/web/webclient/static/webclient/js/plugins/popups.js create mode 100644 evennia/web/webclient/static/webclient/js/plugins/splithandler.js delete mode 100644 evennia/web/webclient/static/webclient/js/splithandler.js diff --git a/evennia/web/webclient/static/webclient/css/webclient.css b/evennia/web/webclient/static/webclient/css/webclient.css index 7a33cfa207..94ac7a0d8d 100644 --- a/evennia/web/webclient/static/webclient/css/webclient.css +++ b/evennia/web/webclient/static/webclient/css/webclient.css @@ -105,6 +105,13 @@ div {margin:0px;} } /* Input field */ +#input { + position: fixed; + bottom: 0; + left: 0; + width: 100%; +} + #inputfield, #inputsizer { height: 100%; background: #000; diff --git a/evennia/web/webclient/static/webclient/js/plugins/default_in.js b/evennia/web/webclient/static/webclient/js/plugins/default_in.js new file mode 100644 index 0000000000..28bfc9f315 --- /dev/null +++ b/evennia/web/webclient/static/webclient/js/plugins/default_in.js @@ -0,0 +1,44 @@ +/* + * + * Evennia Webclient default 'send-text-on-enter-key' IO plugin + * + */ +let defaultin_plugin = (function () { + + // + // handle the default key triggering onSend() + var onKeydown = function (event) { + if ( (event.which === 13) && (!event.shiftKey) ) { // Enter Key without shift + var inputfield = $("#inputfield"); + var outtext = inputfield.val(); + var lines = outtext.trim().replace(/[\r]+/,"\n").replace(/[\n]+/, "\n").split("\n"); + for (var i = 0; i < lines.length; i++) { + plugin_handler.onSend( lines[i].trim() ); + } + inputfield.val(''); + event.preventDefault(); + } + + return true; + } + + // + // Mandatory plugin init function + var init = function () { + // Handle pressing the send button + $("#inputsend") + .bind("click", function (event) { + var e = $.Event( "keydown" ); + e.which = 13; + $('#inputfield').trigger(e); + }); + + console.log('DefaultIn initialized'); + } + + return { + init: init, + onKeydown: onKeydown, + } +})(); +plugin_handler.add('defaultin', defaultin_plugin); diff --git a/evennia/web/webclient/static/webclient/js/plugins/default_out.js b/evennia/web/webclient/static/webclient/js/plugins/default_out.js new file mode 100644 index 0000000000..01be2f9c62 --- /dev/null +++ b/evennia/web/webclient/static/webclient/js/plugins/default_out.js @@ -0,0 +1,60 @@ +/* + * + * Evennia Webclient default outputs plugin + * + */ +let defaultout_plugin = (function () { + + // + // By default add all unclaimed onText messages to the #messagewindow
and scroll + var onText = function (args, kwargs) { + // append message to default pane, then scroll so latest is at the bottom. + var mwin = $("#messagewindow"); + var cls = kwargs == null ? 'out' : kwargs['cls']; + mwin.append("
" + args[0] + "
"); + var scrollHeight = mwin.parent().parent().prop("scrollHeight"); + mwin.parent().parent().animate({ scrollTop: scrollHeight }, 0); + + return true; + } + + // + // By default just show the prompt. + var onPrompt = function (args, kwargs) { + // show prompt + $('#prompt') + .addClass("out") + .html(args[0]); + + return true; + } + + // + // By default just show an error for the Unhandled Event. + var onUnknownCmd = function (args, kwargs) { + var mwin = $("#messagewindow"); + mwin.append( + "
" + + "Error or Unhandled event:
" + + cmdname + ", " + + JSON.stringify(args) + ", " + + JSON.stringify(kwargs) + "

"); + mwin.scrollTop(mwin[0].scrollHeight); + + return true; + } + + // + // Mandatory plugin init function + var init = function () { + console.log('DefaultOut initialized'); + } + + return { + init: init, + onText: onText, + onPrompt: onPrompt, + onUnknownCmd: onUnknownCmd, + } +})(); +plugin_handler.add('defaultout', defaultout_plugin); diff --git a/evennia/web/webclient/static/webclient/js/plugins/default_unload.js b/evennia/web/webclient/static/webclient/js/plugins/default_unload.js new file mode 100644 index 0000000000..42fa41c930 --- /dev/null +++ b/evennia/web/webclient/static/webclient/js/plugins/default_unload.js @@ -0,0 +1,17 @@ +/* + * + * Evennia Webclient default unload plugin + * + */ +let unload_plugin = (function () { + + let onBeforeUnload = function () { + return "You are about to leave the game. Please confirm."; + } + + return { + init: function () {}, + onBeforeUnload: onBeforeUnload, + } +})(); +plugin_handler.add('unload', unload_plugin); diff --git a/evennia/web/webclient/static/webclient/js/plugins/history.js b/evennia/web/webclient/static/webclient/js/plugins/history.js new file mode 100644 index 0000000000..1bef6031cd --- /dev/null +++ b/evennia/web/webclient/static/webclient/js/plugins/history.js @@ -0,0 +1,116 @@ +/* + * + * Evennia Webclient Command History plugin + * + */ +let history_plugin = (function () { + + // Manage history for input line + var history_max = 21; + var history = new Array(); + var history_pos = 0; + + history[0] = ''; // the very latest input is empty for new entry. + + // + // move back in the history + var back = function () { + // step backwards in history stack + history_pos = Math.min(++history_pos, history.length - 1); + return history[history.length - 1 - history_pos]; + } + + // + // move forward in the history + var fwd = function () { + // step forwards in history stack + history_pos = Math.max(--history_pos, 0); + return history[history.length - 1 - history_pos]; + } + + // + // add a new history line + var add = function (input) { + // add a new entry to history, don't repeat latest + if (input && input != history[history.length-2]) { + if (history.length >= history_max) { + history.shift(); // kill oldest entry + } + history[history.length-1] = input; + history[history.length] = ''; + } + // reset the position to the last history entry + history_pos = 0; + } + + // + // Go to the last history line + var end = function () { + // move to the end of the history stack + history_pos = 0; + return history[history.length -1]; + } + + // + // Add input to the scratch line + var scratch = function (input) { + // Put the input into the last history entry (which is normally empty) + // without making the array larger as with add. + // Allows for in-progress editing to be saved. + history[history.length-1] = input; + } + + // Public + + // + // Handle up arrow and down arrow events. + var onKeydown = function(event) { + var code = event.which; + var history_entry = null; + var inputfield = $("#inputfield"); + + if (inputfield[0].selectionStart == inputfield.val().length) { + // Only process up/down arrow if cursor is at the end of the line. + if (code === 38) { // Arrow up + history_entry = back(); + } + else if (code === 40) { // Arrow down + history_entry = fwd(); + } + } + + if (history_entry !== null) { + // Doing a history navigation; replace the text in the input. + inputfield.val(history_entry); + } + else { + // Save the current contents of the input to the history scratch area. + setTimeout(function () { + // Need to wait until after the key-up to capture the value. + scratch(inputfield.val()); + end(); + }, 0); + } + + return false; + } + + // + // Listen for onSend lines to add to history + var onSend = function (line) { + add(line); + } + + // + // Init function + var init = function () { + console.log('History Plugin Initialized.'); + } + + return { + init: init, + onKeydown: onKeydown, + onSend: onSend, + } +})() +plugin_handler.add('history', history_plugin); diff --git a/evennia/web/webclient/static/webclient/js/plugins/notifications.js b/evennia/web/webclient/static/webclient/js/plugins/notifications.js new file mode 100644 index 0000000000..ba68cf9081 --- /dev/null +++ b/evennia/web/webclient/static/webclient/js/plugins/notifications.js @@ -0,0 +1,88 @@ +/* + * + * Desktop Notifications Plugin + * + */ +let notifications_plugin = (function () { + // Notifications + var unread = 0; + var originalTitle = document.title; + var focused = true; + var favico; + + var onBlur = function (e) { + focused = false; + } + + // + // Notifications for unfocused window + var onFocus = function (e) { + focused = true; + document.title = originalTitle; + unread = 0; + favico.badge(0); + } + + // + // on receiving new text from the server, if we are not focused, send a notification to the desktop + var onText = function (args, kwargs) { + if(!focused) { + // Changes unfocused browser tab title to number of unread messages + unread++; + favico.badge(unread); + document.title = "(" + unread + ") " + originalTitle; + if ("Notification" in window) { + if (("notification_popup" in options) && (options["notification_popup"])) { + // There is a Promise-based API for this, but it’s not supported + // in Safari and some older browsers: + // https://developer.mozilla.org/en-US/docs/Web/API/Notification/requestPermission#Browser_compatibility + Notification.requestPermission(function(result) { + if(result === "granted") { + var title = originalTitle === "" ? "Evennia" : originalTitle; + var options = { + body: text.replace(/(<([^>]+)>)/ig,""), + icon: "/static/website/images/evennia_logo.png" + } + + var n = new Notification(title, options); + n.onclick = function(e) { + e.preventDefault(); + window.focus(); + this.close(); + } + } + }); + } + if (("notification_sound" in options) && (options["notification_sound"])) { + var audio = new Audio("/static/webclient/media/notification.wav"); + audio.play(); + } + } + } + + return false; + } + + // + // required init function + var init = function () { + if ("Notification" in window) { + Notification.requestPermission(); + } + + favico = new Favico({ + animation: 'none' + }); + + $(window).blur(onBlur); + $(window).focus(onFocus); + + console.log('Notifications Plugin Initialized.'); + } + + return { + init: init, + onText: onText, + } +})() +plugin_handler.add('notifications', notifications_plugin); diff --git a/evennia/web/webclient/static/webclient/js/plugins/oob.js b/evennia/web/webclient/static/webclient/js/plugins/oob.js new file mode 100644 index 0000000000..55cedc9a3d --- /dev/null +++ b/evennia/web/webclient/static/webclient/js/plugins/oob.js @@ -0,0 +1,35 @@ +/* + * + * OOB Plugin + * enables '##send { "command", [ args ], { kwargs } }' as a way to inject OOB instructions + * + */ +let oob_plugin = (function () { + + // + // Check outgoing text for handtyped/injected JSON OOB instruction + var onSend = function (line) { + if (line.length > 7 && line.substr(0, 7) == "##send ") { + // send a specific oob instruction ["cmdname",[args],{kwargs}] + line = line.slice(7); + var cmdarr = JSON.parse(line); + var cmdname = cmdarr[0]; + var args = cmdarr[1]; + var kwargs = cmdarr[2]; + log(cmdname, args, kwargs); + return (cmdname, args, kwargs); + } + } + + // + // init function + var init = function () { + console.log('OOB Plugin Initialized.'); + } + + return { + init: init, + onSend: onSend, + } +})() +plugin_handler.add('oob', oob_plugin); diff --git a/evennia/web/webclient/static/webclient/js/plugins/options.js b/evennia/web/webclient/static/webclient/js/plugins/options.js new file mode 100644 index 0000000000..d45c0c76bc --- /dev/null +++ b/evennia/web/webclient/static/webclient/js/plugins/options.js @@ -0,0 +1,162 @@ +/* + * + * Evennia Options GUI plugin + * + * This code deals with all of the UI and events related to Options. + * + */ +let options_plugin = (function () { + // + // addOptionsUI + var addOptionsUI = function () { + var content = [ // TODO dynamically create this based on the options{} hash + '

Output display

', + '', + '
', + '', + '
', + '
', + '

Notifications

', + '', + '
', + '', + '
', + ].join("\n"); + + // Create a new options Dialog + plugins['popups'].createDialog( 'optionsdialog', 'Options', content ); + } + + // + // addHelpUI + var addHelpUI = function () { + // Create a new Help Dialog + plugins['popups'].createDialog( 'helpdialog', 'Help', "" ); + } + + // addToolbarButton + var addToolbarButton = function () { + var optionsbutton = $( [ + '', + ].join("") ); + $('#toolbar').append( optionsbutton ); + } + + // + // Opens the options dialog + var doOpenOptions = function () { + if (!Evennia.isConnected()) { + alert("You need to be connected."); + return; + } + + plugins['popups'].togglePopup("#optionsdialog"); + } + + // + // When the user changes a setting from the interface + var onOptionCheckboxChanged = function () { + var name = $(this).data("setting"); + var value = this.checked; + + var changedoptions = {}; + changedoptions[name] = value; + Evennia.msg("webclient_options", [], changedoptions); + + options[name] = value; + } + + // Public functions + + // + // onKeydown check for 'ESC' key. + var onKeydown = function (event) { + var code = event.which; + + if (code === 27) { // Escape key + if ($('#helpdialog').is(':visible')) { + plugins['popups'].closePopup("#helpdialog"); + } else { + plugins['popups'].closePopup("#optionsdialog"); + } + return true; + } + return false; + } + + // + // Called when options settings are sent from server + var onGotOptions = function (args, kwargs) { + options = kwargs; + + $.each(kwargs, function(key, value) { + var elem = $("[data-setting='" + key + "']"); + if (elem.length === 0) { + console.log("Could not find option: " + key); + console.log(args); + console.log(kwargs); + } else { + elem.prop('checked', value); + }; + }); + } + + // + // Called when the user logged in + var onLoggedIn = function (args, kwargs) { + $('#optionsbutton').removeClass('hidden'); + Evennia.msg("webclient_options", [], {}); + } + + // + // Display a "prompt" command from the server + var onPrompt = function (args, kwargs) { + // also display the prompt in the output window if gagging is disabled + if (("gagprompt" in options) && (!options["gagprompt"])) { + plugin_handler.onText(args, kwargs); + } + + // don't claim this Prompt as completed. + return false; + } + + // + // Make sure to close any dialogs on connection lost + var onConnectionClose = function () { + $('#optionsbutton').addClass('hidden'); + plugins['popups'].closePopup("#optionsdialog"); + plugins['popups'].closePopup("#helpdialog"); + } + + // + // Register and init plugin + var init = function () { + // Add GUI components + addOptionsUI(); + addHelpUI(); + + // Add Options toolbar button. + addToolbarButton(); + + // Pressing the settings button + $("#optionsbutton").bind("click", doOpenOptions); + + // Checking a checkbox in the settings dialog + $("[data-setting]").bind("change", onOptionCheckboxChanged); + + console.log('Options Plugin Initialized.'); + } + + return { + init: init, + onKeydown: onKeydown, + onLoggedIn: onLoggedIn, + onGotOptions: onGotOptions, + onPrompt: onPrompt, + onConnectionClose: onConnectionClose, + } +})() +plugin_handler.add('options', options_plugin); diff --git a/evennia/web/webclient/static/webclient/js/plugins/popups.js b/evennia/web/webclient/static/webclient/js/plugins/popups.js new file mode 100644 index 0000000000..7d9667a79f --- /dev/null +++ b/evennia/web/webclient/static/webclient/js/plugins/popups.js @@ -0,0 +1,101 @@ +/* + * Popups GUI functions plugin + */ +let popups_plugin = (function () { + + // + // openPopup + var openPopup = function (dialogname, content) { + var dialog = $(dialogname); + if (!dialog.length) { + console.log("Dialog " + renderto + " not found."); + return; + } + + if (content) { + var contentel = dialog.find(".dialogcontent"); + contentel.html(content); + } + dialog.show(); + } + + // + // closePopup + var closePopup = function (dialogname) { + var dialog = $(dialogname); + dialog.hide(); + } + + // + // togglePopup + var togglePopup = function (dialogname, content) { + var dialog = $(dialogname); + if (dialog.css('display') == 'none') { + openPopup(dialogname, content); + } else { + closePopup(dialogname); + } + } + + // + // createDialog + var createDialog = function (dialogid, dialogtitle, content) { + var dialog = $( [ + '
', + '
'+ dialogtitle +'×
', + '
', + '
'+ content +'
', + '
', + '
', + '
', + ].join("\n") ); + + $('body').append( dialog ); + + $('#'+ dialogid +' .dialogclose').bind('click', function (event) { $('#'+dialogid).hide(); }); + } + + // + // User clicked on a dialog to drag it + var doStartDragDialog = function (event) { + var dialog = $(event.target).closest(".dialog"); + dialog.css('cursor', 'move'); + + var position = dialog.offset(); + var diffx = event.pageX; + var diffy = event.pageY; + + var drag = function(event) { + var y = position.top + event.pageY - diffy; + var x = position.left + event.pageX - diffx; + dialog.offset({top: y, left: x}); + }; + + var undrag = function() { + $(document).unbind("mousemove", drag); + $(document).unbind("mouseup", undrag); + dialog.css('cursor', ''); + } + + $(document).bind("mousemove", drag); + $(document).bind("mouseup", undrag); + } + + // + // required plugin function + var init = function () { + // Makes dialogs draggable + $(".dialogtitle").bind("mousedown", doStartDragDialog); + + console.log('Popups Plugin Initialized.'); + } + + return { + init: init, + openPopup: openPopup, + closePopup: closePopup, + togglePopup: togglePopup, + createDialog: createDialog, + } +})() +plugin_handler.add('popups', popups_plugin); diff --git a/evennia/web/webclient/static/webclient/js/plugins/splithandler.js b/evennia/web/webclient/static/webclient/js/plugins/splithandler.js new file mode 100644 index 0000000000..b55dea7696 --- /dev/null +++ b/evennia/web/webclient/static/webclient/js/plugins/splithandler.js @@ -0,0 +1,368 @@ +/* + * + * Plugin to use split.js to create a basic windowed ui + * + */ +let splithandler_plugin = (function () { + + var num_splits = 0; + var split_panes = {}; + var backout_list = new Array; + + var known_types = new Array(); + + // Exported Functions + + // + // function to assign "Text types to catch" to a pane + var set_pane_types = function (splitpane, types) { + split_panes[splitpane]['types'] = types; + } + + // + // Add buttons to the Evennia webcilent toolbar + function addToolbarButtons () { + var toolbar = $('#toolbar'); + toolbar.append( $('') ); + toolbar.append( $('') ); + toolbar.append( $('') ); + $('#undobutton').hide(); + } + + function addSplitDialog () { + plugins['popups'].createDialog('splitdialog', 'Split Dialog', ''); + } + + function addPaneDialog () { + plugins['popups'].createDialog('panedialog', 'Pane Dialog', ''); + } + + // + // Handle resizing the InputField after a client resize event so that the splits dont get too big. + function resizeInputField () { + var wrapper = $("#inputform") + var input = $("#inputcontrol") + var prompt = $("#prompt") + + input.height( wrapper.height() - (input.offset().top - wrapper.offset().top) ); + } + + // + // Handle resizing of client + function doWindowResize() { + var resizable = $("[data-update-append]"); + var parents = resizable.closest(".split"); + + resizeInputField(); + + parents.animate({ + scrollTop: parents.prop("scrollHeight") + }, 0); + } + + // + // create a new UI split + var dynamic_split = function (splitpane, direction, pane_name1, pane_name2, update_method1, update_method2, sizes) { + // find the sub-div of the pane we are being asked to split + splitpanesub = splitpane + '-sub'; + + // create the new div stack to replace the sub-div with. + var first_div = $( '
' ) + var first_sub = $( '
' ) + var second_div = $( '
' ) + var second_sub = $( '
' ) + + // check to see if this sub-pane contains anything + contents = $('#'+splitpanesub).contents(); + if( contents ) { + // it does, so move it to the first new div-sub (TODO -- selectable between first/second?) + contents.appendTo(first_sub); + } + first_div.append( first_sub ); + second_div.append( second_sub ); + + // update the split_panes array to remove this pane name, but store it for the backout stack + var backout_settings = split_panes[splitpane]; + delete( split_panes[splitpane] ); + + // now vaporize the current split_N-sub placeholder and create two new panes. + $('#'+splitpane).append(first_div); + $('#'+splitpane).append(second_div); + $('#'+splitpane+'-sub').remove(); + + // And split + Split(['#'+pane_name1,'#'+pane_name2], { + direction: direction, + sizes: sizes, + gutterSize: 4, + minSize: [50,50], + }); + + // store our new split sub-divs for future splits/uses by the main UI. + split_panes[pane_name1] = { 'types': [], 'update_method': update_method1 }; + split_panes[pane_name2] = { 'types': [], 'update_method': update_method2 }; + + // add our new split to the backout stack + backout_list.push( {'pane1': pane_name1, 'pane2': pane_name2, 'undo': backout_settings} ); + + $('#undobutton').show(); + } + + // + // Reverse the last UI split + var undo_split = function () { + // pop off the last split pair + var back = backout_list.pop(); + if( !back ) { + return; + } + + if( backout_list.length === 0 ) { + $('#undobutton').hide(); + } + + // Collect all the divs/subs in play + var pane1 = back['pane1']; + var pane2 = back['pane2']; + var pane1_sub = $('#'+pane1+'-sub'); + var pane2_sub = $('#'+pane2+'-sub'); + var pane1_parent = $('#'+pane1).parent(); + var pane2_parent = $('#'+pane2).parent(); + + if( pane1_parent.attr('id') != pane2_parent.attr('id') ) { + // sanity check failed...somebody did something weird...bail out + console.log( pane1 ); + console.log( pane2 ); + console.log( pane1_parent ); + console.log( pane2_parent ); + return; + } + + // create a new sub-pane in the panes parent + var parent_sub = $( '
' ) + + // check to see if the special #messagewindow is in either of our sub-panes. + var msgwindow = pane1_sub.find('#messagewindow') + if( !msgwindow ) { + //didn't find it in pane 1, try pane 2 + msgwindow = pane2_sub.find('#messagewindow') + } + if( msgwindow ) { + // It is, so collect all contents into it instead of our parent_sub div + // then move it to parent sub div, this allows future #messagewindow divs to flow properly + msgwindow.append( pane1_sub.contents() ); + msgwindow.append( pane2_sub.contents() ); + parent_sub.append( msgwindow ); + } else { + //didn't find it, so move the contents of the two panes' sub-panes into the new sub-pane + parent_sub.append( pane1_sub.contents() ); + parent_sub.append( pane2_sub.contents() ); + } + + // clear the parent + pane1_parent.empty(); + + // add the new sub-pane back to the parent div + pane1_parent.append(parent_sub); + + // pull the sub-div's from split_panes + delete split_panes[pane1]; + delete split_panes[pane2]; + + // add our parent pane back into the split_panes list for future splitting + split_panes[pane1_parent.attr('id')] = back['undo']; + } + + // + // UI elements + // + + // + // Draw "Split Controls" Dialog + var onSplitDialog = function () { + var dialog = $("#splitdialogcontent"); + dialog.empty(); + + dialog.append("

Split?

"); + dialog.append(' top/bottom
'); + dialog.append(' side-by-side
'); + + dialog.append("

Split Which Pane?

"); + for ( var pane in split_panes ) { + dialog.append(''+ pane +'
'); + } + + dialog.append("

New Pane Names

"); + dialog.append(''); + dialog.append(''); + + dialog.append("

New First Pane Flow

"); + dialog.append('append
'); + dialog.append('replace
'); + + dialog.append("

New Second Pane Flow

"); + dialog.append('append
'); + dialog.append('replace
'); + + dialog.append('
Split It
'); + + $("#splitclose").bind("click", onSplitDialogClose); + + plugins['popups'].togglePopup("#splitdialog"); + } + + // + // Close "Split Controls" Dialog + var onSplitDialogClose = function () { + var pane = $("input[name=pane]:checked").attr("value"); + var direction = $("input[name=direction]:checked").attr("value"); + var new_pane1 = $("input[name=new_pane1]").val(); + var new_pane2 = $("input[name=new_pane2]").val(); + var flow1 = $("input[name=flow1]:checked").attr("value"); + var flow2 = $("input[name=flow2]:checked").attr("value"); + + if( new_pane1 == "" ) { + new_pane1 = 'pane_'+num_splits; + num_splits++; + } + + if( new_pane2 == "" ) { + new_pane2 = 'pane_'+num_splits; + num_splits++; + } + + if( document.getElementById(new_pane1) ) { + alert('An element: "' + new_pane1 + '" already exists'); + return; + } + + if( document.getElementById(new_pane2) ) { + alert('An element: "' + new_pane2 + '" already exists'); + return; + } + + dynamic_split( pane, direction, new_pane1, new_pane2, flow1, flow2, [50,50] ); + + plugins['popups'].closePopup("#splitdialog"); + } + + // + // Draw "Pane Controls" dialog + var onPaneControlDialog = function () { + var dialog = $("#splitdialogcontent"); + dialog.empty(); + + dialog.append("

Set Which Pane?

"); + for ( var pane in split_panes ) { + dialog.append(''+ pane +'
'); + } + + dialog.append("

Which content types?

"); + for ( var type in known_types ) { + dialog.append(''+ known_types[type] +'
'); + } + + dialog.append('
Make It So
'); + + $("#paneclose").bind("click", onPaneControlDialogClose); + + plugins['popups'].togglePopup("#splitdialog"); + } + + // + // Close "Pane Controls" dialog + var onPaneControlDialogClose = function () { + var pane = $("input[name=pane]:checked").attr("value"); + + var types = new Array; + $('#splitdialogcontent input[type=checkbox]:checked').each(function() { + types.push( $(this).attr('value') ); + }); + + set_pane_types( pane, types ); + + plugins['popups'].closePopup("#splitdialog"); + } + + // + // plugin functions + // + + // + // Accept plugin onText events + var onText = function (args, kwargs) { + if ( kwargs && 'type' in kwargs ) { + var 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); + } + + for ( var key in split_panes) { + var pane = split_panes[key]; + + // is this message type mapped to this pane? + if ( (pane['types'].length > 0) && pane['types'].includes(msgtype) ) { + // yes, so append/replace this pane's inner div with this message + var text_div = $('#'+key+'-sub'); + if ( pane['update_method'] == 'replace' ) { + text_div.html(args[0]) + } else { + text_div.append(args[0]); + var scrollHeight = text_div.parent().prop("scrollHeight"); + text_div.parent().animate({ scrollTop: scrollHeight }, 0); + } + return true; + } + } + } + return false; + } + + // + // Required plugin "init" function + var init = function(settings) { + known_types.push('help'); + + Split(['#main','#input'], { + direction: 'vertical', + sizes: [90,10], + gutterSize: 4, + minSize: [50,50], + }); + + split_panes['main'] = { 'types': [], 'update_method': 'append' }; + + // Create our UI + addToolbarButtons(); + addSplitDialog(); + addPaneDialog(); + + // Register our utility button events + $("#splitbutton").bind("click", onSplitDialog); + $("#panebutton").bind("click", onPaneControlDialog); + $("#undobutton").bind("click", undo_split); + + // Event when client window changes + $(window).bind("resize", doWindowResize); + + $("[data-role-input]").bind("resize", doWindowResize) + .bind("paste", resizeInputField) + .bind("cut", resizeInputField); + + // Event when any key is pressed + $(document).keyup(resizeInputField); + + console.log("Splithandler Plugin Initialized."); + } + + return { + init: init, + onText: onText, + dynamic_split: dynamic_split, + undo_split: undo_split, + set_pane_types: set_pane_types, + } +})() +plugin_handler.add('splithandler', splithandler_plugin); diff --git a/evennia/web/webclient/static/webclient/js/splithandler.js b/evennia/web/webclient/static/webclient/js/splithandler.js deleted file mode 100644 index 81210df854..0000000000 --- a/evennia/web/webclient/static/webclient/js/splithandler.js +++ /dev/null @@ -1,145 +0,0 @@ -// Use split.js to create a basic ui -var SplitHandler = (function () { - var split_panes = {}; - var backout_list = new Array; - - var set_pane_types = function(splitpane, types) { - split_panes[splitpane]['types'] = types; - } - - - var dynamic_split = function(splitpane, direction, pane_name1, pane_name2, update_method1, update_method2, sizes) { - // find the sub-div of the pane we are being asked to split - splitpanesub = splitpane + '-sub'; - - // create the new div stack to replace the sub-div with. - var first_div = $( '
' ) - var first_sub = $( '
' ) - var second_div = $( '
' ) - var second_sub = $( '
' ) - - // check to see if this sub-pane contains anything - contents = $('#'+splitpanesub).contents(); - if( contents ) { - // it does, so move it to the first new div-sub (TODO -- selectable between first/second?) - contents.appendTo(first_sub); - } - first_div.append( first_sub ); - second_div.append( second_sub ); - - // update the split_panes array to remove this pane name, but store it for the backout stack - var backout_settings = split_panes[splitpane]; - delete( split_panes[splitpane] ); - - // now vaporize the current split_N-sub placeholder and create two new panes. - $('#'+splitpane).append(first_div); - $('#'+splitpane).append(second_div); - $('#'+splitpane+'-sub').remove(); - - // And split - Split(['#'+pane_name1,'#'+pane_name2], { - direction: direction, - sizes: sizes, - gutterSize: 4, - minSize: [50,50], - }); - - // store our new split sub-divs for future splits/uses by the main UI. - split_panes[pane_name1] = { 'types': [], 'update_method': update_method1 }; - split_panes[pane_name2] = { 'types': [], 'update_method': update_method2 }; - - // add our new split to the backout stack - backout_list.push( {'pane1': pane_name1, 'pane2': pane_name2, 'undo': backout_settings} ); - } - - - var undo_split = function() { - // pop off the last split pair - var back = backout_list.pop(); - if( !back ) { - return; - } - - // Collect all the divs/subs in play - var pane1 = back['pane1']; - var pane2 = back['pane2']; - var pane1_sub = $('#'+pane1+'-sub'); - var pane2_sub = $('#'+pane2+'-sub'); - var pane1_parent = $('#'+pane1).parent(); - var pane2_parent = $('#'+pane2).parent(); - - if( pane1_parent.attr('id') != pane2_parent.attr('id') ) { - // sanity check failed...somebody did something weird...bail out - console.log( pane1 ); - console.log( pane2 ); - console.log( pane1_parent ); - console.log( pane2_parent ); - return; - } - - // create a new sub-pane in the panes parent - var parent_sub = $( '
' ) - - // check to see if the special #messagewindow is in either of our sub-panes. - var msgwindow = pane1_sub.find('#messagewindow') - if( !msgwindow ) { - //didn't find it in pane 1, try pane 2 - msgwindow = pane2_sub.find('#messagewindow') - } - if( msgwindow ) { - // It is, so collect all contents into it instead of our parent_sub div - // then move it to parent sub div, this allows future #messagewindow divs to flow properly - msgwindow.append( pane1_sub.contents() ); - msgwindow.append( pane2_sub.contents() ); - parent_sub.append( msgwindow ); - } else { - //didn't find it, so move the contents of the two panes' sub-panes into the new sub-pane - parent_sub.append( pane1_sub.contents() ); - parent_sub.append( pane2_sub.contents() ); - } - - // clear the parent - pane1_parent.empty(); - - // add the new sub-pane back to the parent div - pane1_parent.append(parent_sub); - - // pull the sub-div's from split_panes - delete split_panes[pane1]; - delete split_panes[pane2]; - - // add our parent pane back into the split_panes list for future splitting - split_panes[pane1_parent.attr('id')] = back['undo']; - } - - - var init = function(settings) { - //change Mustache tags to ruby-style (Django gets mad otherwise) - var customTags = [ '<%', '%>' ]; - Mustache.tags = customTags; - - var input_template = $('#input-template').html(); - Mustache.parse(input_template); - - Split(['#main','#input'], { - direction: 'vertical', - sizes: [90,10], - gutterSize: 4, - minSize: [50,50], - }); - - split_panes['main'] = { 'types': [], 'update_method': 'append' }; - - var input_render = Mustache.render(input_template); - $('[data-role-input]').html(input_render); - console.log("SplitHandler initialized"); - } - - return { - init: init, - set_pane_types: set_pane_types, - dynamic_split: dynamic_split, - split_panes: split_panes, - undo_split: undo_split, - } -})(); diff --git a/evennia/web/webclient/static/webclient/js/webclient_gui.js b/evennia/web/webclient/static/webclient/js/webclient_gui.js index e1ed4d31fd..146d4a5268 100644 --- a/evennia/web/webclient/static/webclient/js/webclient_gui.js +++ b/evennia/web/webclient/static/webclient/js/webclient_gui.js @@ -5,625 +5,285 @@ * This is used in conjunction with the main evennia.js library, which * handles all the communication with the Server. * - * The job of this code is to create listeners to subscribe to evennia - * messages, via Evennia.emitter.on(cmdname, listener) and to handle - * input from the user and send it to - * Evennia.msg(cmdname, args, kwargs, [callback]). + * The job of this code is to coordinate between listeners subscribed to + * evennia messages and any registered plugins that want to process those + * messages and send data back to Evennia + * + * This is done via Evennia.emitter.on(cmdname, listener) and calling + * each plugin's init() function to give each plugin a chance to register + * input handlers or other events on startup. + * + * Once a plugin has determined it wants to send a message back to the + * server, it generates an onSend() function event which allows all + * other plugins a chance to modify the event and then uses + * Evennia.msg(cmdname, args, kwargs, [callback]) to finally send the data. * */ -(function () { -"use strict" - -var num_splits = 0; //unique id counter for default split-panel names - -var options = {}; - -var known_types = new Array(); - known_types.push('help'); - // -// GUI Elements +// Global Plugins system // +var options = {}; // Global "settings" object that all plugins can use to + // save/pass data to each other and the server. + // format should match: + // { 'plugin_name': { 'option_key': value, ... }, ... } -// Manage history for input line -var input_history = function() { - var history_max = 21; - var history = new Array(); - var history_pos = 0; - - history[0] = ''; // the very latest input is empty for new entry. - - var back = function () { - // step backwards in history stack - history_pos = Math.min(++history_pos, history.length - 1); - return history[history.length - 1 - history_pos]; - }; - var fwd = function () { - // step forwards in history stack - history_pos = Math.max(--history_pos, 0); - return history[history.length - 1 - history_pos]; - }; - var add = function (input) { - // add a new entry to history, don't repeat latest - if (input && input != history[history.length-2]) { - if (history.length >= history_max) { - history.shift(); // kill oldest entry - } - history[history.length-1] = input; - history[history.length] = ''; - }; - // reset the position to the last history entry - history_pos = 0; - }; - var end = function () { - // move to the end of the history stack - history_pos = 0; - return history[history.length -1]; - } - - var scratch = function (input) { - // Put the input into the last history entry (which is normally empty) - // without making the array larger as with add. - // Allows for in-progress editing to be saved. - history[history.length-1] = input; - } - - return {back: back, - fwd: fwd, - add: add, - end: end, - scratch: scratch} -}(); - -function openPopup(dialogname, content) { - var dialog = $(dialogname); - if (!dialog.length) { - console.log("Dialog " + renderto + " not found."); - return; - } - - if (content) { - var contentel = dialog.find(".dialogcontent"); - contentel.html(content); - } - dialog.show(); -} - -function closePopup(dialogname) { - var dialog = $(dialogname); - dialog.hide(); -} - -function togglePopup(dialogname, content) { - var dialog = $(dialogname); - if (dialog.css('display') == 'none') { - openPopup(dialogname, content); - } else { - closePopup(dialogname); - } -} +var plugins = {}; // Global plugin objects by name. + // Each must have an init() function. // -// GUI Event Handlers +// Global plugin_handler // +var plugin_handler = (function () { + "use strict" -// Grab text from inputline and send to Evennia -function doSendText() { - console.log("sending text"); - if (!Evennia.isConnected()) { - var reconnect = confirm("Not currently connected. Reconnect?"); - if (reconnect) { - onText(["Attempting to reconnnect..."], {cls: "sys"}); - Evennia.connect(); - } - // Don't try to send anything until the connection is back. - return; - } - var inputfield = $("#inputfield"); - var outtext = inputfield.val(); - var lines = outtext.trim().replace(/[\r]+/,"\n").replace(/[\n]+/, "\n").split("\n"); - for (var i = 0; i < lines.length; i++) { - var line = lines[i].trim(); - if (line.length > 7 && line.substr(0, 7) == "##send ") { - // send a specific oob instruction ["cmdname",[args],{kwargs}] - line = line.slice(7); - var cmdarr = JSON.parse(line); - var cmdname = cmdarr[0]; - var args = cmdarr[1]; - var kwargs = cmdarr[2]; - log(cmdname, args, kwargs); - Evennia.msg(cmdname, args, kwargs); - } else { - input_history.add(line); - inputfield.val(""); - Evennia.msg("text", [line], {}); - } - } -} + var ordered_plugins = new Array; // plugins in loaded order -// Opens the options dialog -function doOpenOptions() { - if (!Evennia.isConnected()) { - alert("You need to be connected."); - return; + // + // Plugin Support Functions + // + + // Add a new plugin + var add = function (name, plugin) { + plugins[name] = plugin; + ordered_plugins.push( plugin ); } - togglePopup("#optionsdialog"); -} -// Closes the currently open dialog -function doCloseDialog(event) { - var dialog = $(event.target).closest(".dialog"); - dialog.hide(); -} + // + // GUI Event Handlers + // -// catch all keyboard input, handle special chars -function onKeydown (event) { - var code = event.which; - var history_entry = null; - var inputfield = $("#inputfield"); - if (code === 9) { - return; - } - - //inputfield.focus(); - - if (code === 13) { // Enter key sends text - doSendText(); - event.preventDefault(); - } - else if (inputfield[0].selectionStart == inputfield.val().length) { - // Only process up/down arrow if cursor is at the end of the line. - if (code === 38) { // Arrow up - history_entry = input_history.back(); - } - else if (code === 40) { // Arrow down - history_entry = input_history.fwd(); - } - } - - if (code === 27) { // Escape key - if ($('#helpdialog').is(':visible')) { - closePopup("#helpdialog"); - } else { - closePopup("#optionsdialog"); - } - } - - if (history_entry !== null) { - // Doing a history navigation; replace the text in the input. - inputfield.val(history_entry); - event.preventDefault(); - } - else { - // Save the current contents of the input to the history scratch area. - setTimeout(function () { - // Need to wait until after the key-up to capture the value. - input_history.scratch(inputfield.val()); - input_history.end(); - }, 0); - } -}; - -function onKeyPress (event) { - // Prevent carriage returns inside the input area. - if (event.which === 13) { - event.preventDefault(); - } -} - -var resizeInputField = function () { - return function() { - var wrapper = $("#inputform") - var input = $("#inputcontrol") - var prompt = $("#prompt") - - input.height(wrapper.height() - (input.offset().top - wrapper.offset().top)); - } -}(); - -// Handle resizing of client -function doWindowResize() { - resizeInputField(); - var resizable = $("[data-update-append]"); - var parents = resizable.closest(".split") - parents.animate({ - scrollTop: parents.prop("scrollHeight") - }, 0); -} - -// Handle text coming from the server -function onText(args, kwargs) { - var use_default_pane = true; - - if ( kwargs && 'type' in kwargs ) { - var 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); - } - - // pass this message to each pane that has this msgtype mapped - if( SplitHandler ) { - for ( var key in SplitHandler.split_panes) { - var pane = SplitHandler.split_panes[key]; - // is this message type mapped to this pane? - if ( (pane['types'].length > 0) && pane['types'].includes(msgtype) ) { - // yes, so append/replace this pane's inner div with this message - var text_div = $('#'+key+'-sub'); - if ( pane['update_method'] == 'replace' ) { - text_div.html(args[0]) - } else { - text_div.append(args[0]); - var scrollHeight = text_div.parent().prop("scrollHeight"); - text_div.parent().animate({ scrollTop: scrollHeight }, 0); - } - // record sending this message to a pane, no need to update the default div - use_default_pane = false; + // catch all keyboard input, handle special chars + var onKeydown = function (event) { + // cycle through each plugin's keydown + for( let n=0; n < ordered_plugins.length; n++ ) { + let plugin = ordered_plugins[n]; + // does this plugin handle keydown events? + if( 'onKeydown' in plugin ) { + // yes, does this plugin claim this event exclusively? + if( plugin.onKeydown(event) ) { + // 'true' claims this event has been handled + return; } } } + console.log('NO plugin handled this Keydown'); } - // append message to default pane, then scroll so latest is at the bottom. - if(use_default_pane) { - var mwin = $("#messagewindow"); - var cls = kwargs == null ? 'out' : kwargs['cls']; - mwin.append("
" + args[0] + "
"); - var scrollHeight = mwin.parent().parent().prop("scrollHeight"); - mwin.parent().parent().animate({ scrollTop: scrollHeight }, 0); - onNewLine(args[0], null); - } -} - -// Handle prompt output from the server -function onPrompt(args, kwargs) { - // show prompt - $('#prompt') - .addClass("out") - .html(args[0]); - doWindowResize(); - - // also display the prompt in the output window if gagging is disabled - if (("gagprompt" in options) && (!options["gagprompt"])) { - onText(args, kwargs); - } -} - -// Called when the user logged in -function onLoggedIn() { - $('#optionsbutton').removeClass('hidden'); - Evennia.msg("webclient_options", [], {}); -} - -// Called when a setting changed -function onGotOptions(args, kwargs) { - options = kwargs; - - $.each(kwargs, function(key, value) { - var elem = $("[data-setting='" + key + "']"); - if (elem.length === 0) { - console.log("Could not find option: " + key); - } else { - elem.prop('checked', value); - }; - }); -} - -// Called when the user changed a setting from the interface -function onOptionCheckboxChanged() { - var name = $(this).data("setting"); - var value = this.checked; - - var changedoptions = {}; - changedoptions[name] = value; - Evennia.msg("webclient_options", [], changedoptions); - - options[name] = value; -} - -// Silences events we don't do anything with. -function onSilence(cmdname, args, kwargs) {} - -// Handle the server connection closing -function onConnectionClose(conn_name, evt) { - $('#optionsbutton').addClass('hidden'); - closePopup("#optionsdialog"); - onText(["The connection was closed or lost."], {'cls': 'err'}); -} - -// Handle unrecognized commands from server -function onDefault(cmdname, args, kwargs) { - var mwin = $("#messagewindow"); - mwin.append( - "
" - + "Error or Unhandled event:
" - + cmdname + ", " - + JSON.stringify(args) + ", " - + JSON.stringify(kwargs) + "

"); - mwin.scrollTop(mwin[0].scrollHeight); -} - -// Ask if user really wants to exit session when closing -// the tab or reloading the page. Note: the message is not shown -// in Firefox, there it's a standard error. -function onBeforeUnload() { - return "You are about to leave the game. Please confirm."; -} - -// Notifications -var unread = 0; -var originalTitle = document.title; -var focused = true; -var favico; - -function onBlur(e) { - focused = false; -} - -// Notifications for unfocused window -function onFocus(e) { - focused = true; - document.title = originalTitle; - unread = 0; - favico.badge(0); -} - -function onNewLine(text, originator) { - if(!focused) { - // Changes unfocused browser tab title to number of unread messages - unread++; - favico.badge(unread); - document.title = "(" + unread + ") " + originalTitle; - if ("Notification" in window){ - if (("notification_popup" in options) && (options["notification_popup"])) { - // There is a Promise-based API for this, but it’s not supported - // in Safari and some older browsers: - // https://developer.mozilla.org/en-US/docs/Web/API/Notification/requestPermission#Browser_compatibility - Notification.requestPermission(function(result) { - if(result === "granted") { - var title = originalTitle === "" ? "Evennia" : originalTitle; - var options = { - body: text.replace(/(<([^>]+)>)/ig,""), - icon: "/static/website/images/evennia_logo.png" - } - - var n = new Notification(title, options); - n.onclick = function(e) { - e.preventDefault(); - window.focus(); - this.close(); - } + // Ask if user really wants to exit session when closing + // the tab or reloading the page. Note: the message is not shown + // in Firefox, there it's a standard error. + var onBeforeUnload = function () { + // cycle through each plugin to look for unload handlers + for( let n=0; n < ordered_plugins.length; n++ ) { + let plugin = ordered_plugins[n]; + if( 'onBeforeUnload' in plugin ) { + plugin.onBeforeUnload(); } - }); - } - if (("notification_sound" in options) && (options["notification_sound"])) { - var audio = new Audio("/static/webclient/media/notification.wav"); - audio.play(); - } - } - } -} - -// User clicked on a dialog to drag it -function doStartDragDialog(event) { - var dialog = $(event.target).closest(".dialog"); - dialog.css('cursor', 'move'); - - var position = dialog.offset(); - var diffx = event.pageX; - var diffy = event.pageY; - - var drag = function(event) { - var y = position.top + event.pageY - diffy; - var x = position.left + event.pageX - diffx; - dialog.offset({top: y, left: x}); - }; - - var undrag = function() { - $(document).unbind("mousemove", drag); - $(document).unbind("mouseup", undrag); - dialog.css('cursor', ''); + } } - $(document).bind("mousemove", drag); - $(document).bind("mouseup", undrag); -} -function onSplitDialogClose() { - var pane = $("input[name=pane]:checked").attr("value"); - var direction = $("input[name=direction]:checked").attr("value"); - var new_pane1 = $("input[name=new_pane1]").val(); - var new_pane2 = $("input[name=new_pane2]").val(); - var flow1 = $("input[name=flow1]:checked").attr("value"); - var flow2 = $("input[name=flow2]:checked").attr("value"); + // + // Evennia Public Event Handlers + // - if( new_pane1 == "" ) { - new_pane1 = 'pane_'+num_splits; - num_splits++; + // Handle onLoggedIn from the server + var onLoggedIn = function (args, kwargs) { + for( let n=0; n < ordered_plugins.length; n++ ) { + let plugin = ordered_plugins[n]; + if( 'onLoggedIn' in plugin ) { + plugin.onLoggedIn(args, kwargs); + } + } } - if( new_pane2 == "" ) { - new_pane2 = 'pane_'+num_splits; - num_splits++; + + // Handle onGotOptions from the server + var onGotOptions = function (args, kwargs) { + // does any plugin handle Options? + for( let n=0; n < ordered_plugins.length; n++ ) { + let plugin = ordered_plugins[n]; + if( 'onGotOptions' in plugin ) { + plugin.onGotOptions(args, kwargs); + } + } } - if( document.getElementById(new_pane1) ) { - alert('An element: "' + new_pane1 + '" already exists'); - return; + + // Handle text coming from the server + var onText = function (args, kwargs) { + // does this plugin handle this onText event? + for( let n=0; n < ordered_plugins.length; n++ ) { + let plugin = ordered_plugins[n]; + if( 'onText' in plugin ) { + if( plugin.onText(args, kwargs) ) { + // True -- means this plugin claims this Text exclusively. + return; + } + } + } + console.log('NO plugin handled this Text'); } - if( document.getElementById(new_pane2) ) { - alert('An element: "' + new_pane2 + '" already exists'); - return; + + // Handle prompt output from the server + var onPrompt = function (args, kwargs) { + // does this plugin handle this onPrompt event? + for( let n=0; n < ordered_plugins.length; n++ ) { + let plugin = ordered_plugins[n]; + if( 'onPrompt' in plugin ) { + if( plugin.onPrompt(args, kwargs) ) { + // True -- means this plugin claims this Prompt exclusively. + return; + } + } + } + console.log('NO plugin handled this Prompt'); } - SplitHandler.dynamic_split( pane, direction, new_pane1, new_pane2, flow1, flow2, [50,50] ); - closePopup("#splitdialog"); -} - -function onSplitDialog() { - var dialog = $("#splitdialogcontent"); - dialog.empty(); - - dialog.append("

Split?

"); - dialog.append(' top/bottom
'); - dialog.append(' side-by-side
'); - - dialog.append("

Split Which Pane?

"); - for ( var pane in SplitHandler.split_panes ) { - dialog.append(''+ pane +'
'); + // Handle unrecognized commands from server + var onDefault = function (cmdname, args, kwargs) { + // does this plugin handle this UnknownCmd? + for( let n=0; n < ordered_plugins.length; n++ ) { + let plugin = ordered_plugins[n]; + if( 'onUnknownCmd' in plugin ) { + if( plugin.onUnknownCmd(args, kwargs) ) { + // True -- means this plugin claims this UnknownCmd exclusively. + return; + } + } + } + console.log('NO plugin handled this Unknown Evennia Command'); } - dialog.append("

New Pane Names

"); - dialog.append(''); - dialog.append(''); - dialog.append("

New First Pane

"); - dialog.append('append new incoming messages
'); - dialog.append('replace old messages with new ones
'); + // Handle the server connection closing + var onConnectionClose = function (args, kwargs) { + // give every plugin a chance to do stuff onConnectionClose + for( let n=0; n < ordered_plugins.length; n++ ) { + let plugin = ordered_plugins[n]; + if( 'onConnectionClose' in plugin ) { + plugin.onConnectionClose(args, kwargs); + } + } - dialog.append("

New Second Pane

"); - dialog.append('append new incoming messages
'); - dialog.append('replace old messages with new ones
'); - - dialog.append('
Split It
'); - - $("#splitclose").bind("click", onSplitDialogClose); - - togglePopup("#splitdialog"); -} - -function onPaneControlDialogClose() { - var pane = $("input[name=pane]:checked").attr("value"); - - var types = new Array; - $('#splitdialogcontent input[type=checkbox]:checked').each(function() { - types.push( $(this).attr('value') ); - }); - - SplitHandler.set_pane_types( pane, types ); - - closePopup("#splitdialog"); -} - -function onPaneControlDialog() { - var dialog = $("#splitdialogcontent"); - dialog.empty(); - - dialog.append("

Set Which Pane?

"); - for ( var pane in SplitHandler.split_panes ) { - dialog.append(''+ pane +'
'); + onText(["The connection was closed or lost."], {'cls': 'err'}); } - dialog.append("

Which content types?

"); - for ( var type in known_types ) { - dialog.append(''+ known_types[type] +'
'); + + // Silences events we don't do anything with. + var onSilence = function (cmdname, args, kwargs) {} + + + // + // Global onSend() function to iterate through all plugins before sending text to the server. + // This can be called by other plugins for "Triggers", , and other automated sends + // + var onSend = function (line) { + if (!Evennia.isConnected()) { + var reconnect = confirm("Not currently connected. Reconnect?"); + if (reconnect) { + onText(["Attempting to reconnnect..."], {cls: "sys"}); + Evennia.connect(); + } + // Don't try to send anything until the connection is back. + return; + } + + // default output command + var cmd = { + command: "text", + args: [ line ], + kwargs: {} + }; + + // Give each plugin a chance to use/modify the outgoing command for aliases/history/etc + for( let n=0; n < ordered_plugins.length; n++ ) { + let plugin = ordered_plugins[n]; + if( 'onSend' in plugin ) { + var outCmd = plugin.onSend(line); + if( outCmd ) { + cmd = outCmd; + } + } + } + + // console.log('sending: ' + cmd.command + ', [' + cmd.args[0].toString() + '], ' + cmd.kwargs.toString() ); + Evennia.msg(cmd.command, cmd.args, cmd.kwargs); } - dialog.append('
Make It So
'); - $("#paneclose").bind("click", onPaneControlDialogClose); + // + // call each plugins' init function (the only required function) + // + var init = function () { + for( let n=0; n < ordered_plugins.length; n++ ) { + ordered_plugins[n].init(); + } + } + + + return { + add: add, + onKeydown: onKeydown, + onBeforeUnload: onBeforeUnload, + onLoggedIn: onLoggedIn, + onText: onText, + onGotOptions: onGotOptions, + onPrompt: onPrompt, + onDefault: onDefault, + onSilence: onSilence, + onConnectionClose: onConnectionClose, + onSend: onSend, + init: init, + } +})(); - togglePopup("#splitdialog"); -} // -// Register Events +// Webclient Initialization // // Event when client finishes loading $(document).ready(function() { - - if( SplitHandler ) { - SplitHandler.init(); - $("#splitbutton").bind("click", onSplitDialog); - $("#panebutton").bind("click", onPaneControlDialog); - $("#undobutton").bind("click", SplitHandler.undo_split); - $("#optionsbutton").hide(); - } else { - $("#splitbutton").hide(); - $("#panebutton").hide(); - $("#undobutton").hide(); - } - - if ("Notification" in window) { - Notification.requestPermission(); - } - - favico = new Favico({ - animation: 'none' - }); - - // Event when client window changes - $(window).bind("resize", doWindowResize); - - $(window).blur(onBlur); - $(window).focus(onFocus); - - //$(document).on("visibilitychange", onVisibilityChange); - - $("[data-role-input]").bind("resize", doWindowResize) - .keypress(onKeyPress) - .bind("paste", resizeInputField) - .bind("cut", resizeInputField); - - // Event when any key is pressed - $(document).keydown(onKeydown) - .keyup(resizeInputField); - - // Pressing the send button - $("#inputsend").bind("click", doSendText); - - // Pressing the settings button - $("#optionsbutton").bind("click", doOpenOptions); - - // Checking a checkbox in the settings dialog - $("[data-setting]").bind("change", onOptionCheckboxChanged); - - // Pressing the close button on a dialog - $(".dialogclose").bind("click", doCloseDialog); - - // Makes dialogs draggable - $(".dialogtitle").bind("mousedown", doStartDragDialog); - // This is safe to call, it will always only // initialize once. Evennia.init(); - // register listeners - Evennia.emitter.on("text", onText); - Evennia.emitter.on("prompt", onPrompt); - Evennia.emitter.on("default", onDefault); - Evennia.emitter.on("connection_close", onConnectionClose); - Evennia.emitter.on("logged_in", onLoggedIn); - Evennia.emitter.on("webclient_options", onGotOptions); - // silence currently unused events - Evennia.emitter.on("connection_open", onSilence); - Evennia.emitter.on("connection_error", onSilence); - // Handle pressing the send button - $("#inputsend").bind("click", doSendText); + // register listeners + Evennia.emitter.on("logged_in", plugin_handler.onLoggedIn); + Evennia.emitter.on("text", plugin_handler.onText); + Evennia.emitter.on("webclient_options", plugin_handler.onGotOptions); + Evennia.emitter.on("prompt", plugin_handler.onPrompt); + Evennia.emitter.on("default", plugin_handler.onDefault); + Evennia.emitter.on("connection_close", plugin_handler.onConnectionClose); + + // silence currently unused events + Evennia.emitter.on("connection_open", plugin_handler.onSilence); + Evennia.emitter.on("connection_error", plugin_handler.onSilence); + // Event when closing window (have to have Evennia initialized) - $(window).bind("beforeunload", onBeforeUnload); + $(window).bind("beforeunload", plugin_handler.onBeforeUnload); $(window).bind("unload", Evennia.connection.close); - doWindowResize(); + // Event when any key is pressed + $(document).keydown(plugin_handler.onKeydown) + // set an idle timer to send idle every 3 minutes, // to avoid proxy servers timing out on us - setInterval(function() { - // Connect to server - Evennia.msg("text", ["idle"], {}); - }, - 60000*3 + setInterval( function() { // Connect to server + Evennia.msg("text", ["idle"], {}); + }, + 60000*3 ); - console.log("Completed GUI setup"); + // Initialize all plugins + plugin_handler.init(); + console.log("Completed Webclient setup"); }); - -})(); diff --git a/evennia/web/webclient/templates/webclient/base.html b/evennia/web/webclient/templates/webclient/base.html index a5c65fad2c..1b506c8cd5 100644 --- a/evennia/web/webclient/templates/webclient/base.html +++ b/evennia/web/webclient/templates/webclient/base.html @@ -63,15 +63,21 @@ JQuery available. - - {% block guilib_import %} + + + + + + + + {% endblock %} diff --git a/evennia/web/webclient/templates/webclient/webclient.html b/evennia/web/webclient/templates/webclient/webclient.html index 74bef631cf..e2aed49ecc 100644 --- a/evennia/web/webclient/templates/webclient/webclient.html +++ b/evennia/web/webclient/templates/webclient/webclient.html @@ -6,67 +6,19 @@ - guilib_import - for using your own gui lib --> - {% block client %} -
- - - - -
- + +
+ +
- -
- - -
-
Split Pane×
-
-
-
-
-
- -
-
Options×
-
-
-

Output display

-
-
-
-

Notifications

-
-
-
-
-
- -
-
Help×
-
-
-
-
-
- - - - - - +
{% endblock %} {% block scripts %}