Merge branch 'develop-plugin-based-webclient' of https://github.com/friarzen/evennia into friarzen-develop-plugin-based-webclient
This commit is contained in:
commit
c6fa04407c
14 changed files with 1230 additions and 759 deletions
|
|
@ -105,6 +105,13 @@ div {margin:0px;}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Input field */
|
/* Input field */
|
||||||
|
#input {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
#inputfield, #inputsizer {
|
#inputfield, #inputsizer {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: #000;
|
background: #000;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* Evennia Webclient default 'send-text-on-enter-key' IO plugin
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
let defaultin_plugin = (function () {
|
||||||
|
|
||||||
|
//
|
||||||
|
// handle the default <enter> 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);
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* Evennia Webclient default outputs plugin
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
let defaultout_plugin = (function () {
|
||||||
|
|
||||||
|
//
|
||||||
|
// By default add all unclaimed onText messages to the #messagewindow <div> 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("<div class='" + cls + "'>" + args[0] + "</div>");
|
||||||
|
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(
|
||||||
|
"<div class='msg err'>"
|
||||||
|
+ "Error or Unhandled event:<br>"
|
||||||
|
+ cmdname + ", "
|
||||||
|
+ JSON.stringify(args) + ", "
|
||||||
|
+ JSON.stringify(kwargs) + "<p></div>");
|
||||||
|
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);
|
||||||
|
|
@ -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);
|
||||||
116
evennia/web/webclient/static/webclient/js/plugins/history.js
Normal file
116
evennia/web/webclient/static/webclient/js/plugins/history.js
Normal file
|
|
@ -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);
|
||||||
|
|
@ -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);
|
||||||
35
evennia/web/webclient/static/webclient/js/plugins/oob.js
Normal file
35
evennia/web/webclient/static/webclient/js/plugins/oob.js
Normal file
|
|
@ -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);
|
||||||
162
evennia/web/webclient/static/webclient/js/plugins/options.js
Normal file
162
evennia/web/webclient/static/webclient/js/plugins/options.js
Normal file
|
|
@ -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
|
||||||
|
'<h3>Output display</h3>',
|
||||||
|
'<label><input type="checkbox" data-setting="gagprompt" value="value">Don\'t echo prompts to the main text area</label>',
|
||||||
|
'<br />',
|
||||||
|
'<label><input type="checkbox" data-setting="helppopup" value="value">Open help in popup window</label>',
|
||||||
|
'<br />',
|
||||||
|
'<hr />',
|
||||||
|
'<h3>Notifications</h3>',
|
||||||
|
'<label><input type="checkbox" data-setting="notification_popup" value="value">Popup notification</label>',
|
||||||
|
'<br />',
|
||||||
|
'<label><input type="checkbox" data-setting="notification_sound" value="value">Play a sound</label>',
|
||||||
|
'<br />',
|
||||||
|
].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 = $( [
|
||||||
|
'<button id="optionsbutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">',
|
||||||
|
'⚙',
|
||||||
|
'<span class="sr-only sr-only-focusable">Settings</span>',
|
||||||
|
'</button>',
|
||||||
|
].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);
|
||||||
101
evennia/web/webclient/static/webclient/js/plugins/popups.js
Normal file
101
evennia/web/webclient/static/webclient/js/plugins/popups.js
Normal file
|
|
@ -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 = $( [
|
||||||
|
'<div id="'+ dialogid +'" class="dialog">',
|
||||||
|
' <div class="dialogtitle">'+ dialogtitle +'<span class="dialogclose">×</span></div>',
|
||||||
|
' <div class="dialogcontentparent">',
|
||||||
|
' <div id="'+ dialogid +'content" class="dialogcontent">'+ content +'</div>',
|
||||||
|
' </div>',
|
||||||
|
' </div>',
|
||||||
|
'</div>',
|
||||||
|
].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);
|
||||||
|
|
@ -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( $('<button id="splitbutton" type="button">⇹</button>') );
|
||||||
|
toolbar.append( $('<button id="panebutton" type="button">⚙</button>') );
|
||||||
|
toolbar.append( $('<button id="undobutton" type="button">↶</button>') );
|
||||||
|
$('#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 = $( '<div id="'+pane_name1+'" class="split split-'+direction+'" />' )
|
||||||
|
var first_sub = $( '<div id="'+pane_name1+'-sub" class="split-sub" />' )
|
||||||
|
var second_div = $( '<div id="'+pane_name2+'" class="split split-'+direction+'" />' )
|
||||||
|
var second_sub = $( '<div id="'+pane_name2+'-sub" class="split-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 = $( '<div id="'+pane1_parent.attr('id')+'-sub" class="split-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("<h3>Split?</h3>");
|
||||||
|
dialog.append('<input type="radio" name="direction" value="vertical" checked> top/bottom<br />');
|
||||||
|
dialog.append('<input type="radio" name="direction" value="horizontal"> side-by-side<br />');
|
||||||
|
|
||||||
|
dialog.append("<h3>Split Which Pane?</h3>");
|
||||||
|
for ( var pane in split_panes ) {
|
||||||
|
dialog.append('<input type="radio" name="pane" value="'+ pane +'">'+ pane +'<br />');
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.append("<h3>New Pane Names</h3>");
|
||||||
|
dialog.append('<input type="text" name="new_pane1" value="" />');
|
||||||
|
dialog.append('<input type="text" name="new_pane2" value="" />');
|
||||||
|
|
||||||
|
dialog.append("<h3>New First Pane Flow</h3>");
|
||||||
|
dialog.append('<input type="radio" name="flow1" value="append" checked>append<br />');
|
||||||
|
dialog.append('<input type="radio" name="flow1" value="replace">replace<br />');
|
||||||
|
|
||||||
|
dialog.append("<h3>New Second Pane Flow</h3>");
|
||||||
|
dialog.append('<input type="radio" name="flow2" value="append" checked>append<br />');
|
||||||
|
dialog.append('<input type="radio" name="flow2" value="replace">replace<br />');
|
||||||
|
|
||||||
|
dialog.append('<div id="splitclose" class="button">Split It</div>');
|
||||||
|
|
||||||
|
$("#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("<h3>Set Which Pane?</h3>");
|
||||||
|
for ( var pane in split_panes ) {
|
||||||
|
dialog.append('<input type="radio" name="pane" value="'+ pane +'">'+ pane +'<br />');
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.append("<h3>Which content types?</h3>");
|
||||||
|
for ( var type in known_types ) {
|
||||||
|
dialog.append('<input type="checkbox" value="'+ known_types[type] +'">'+ known_types[type] +'<br />');
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.append('<div id="paneclose" class="button">Make It So</div>');
|
||||||
|
|
||||||
|
$("#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);
|
||||||
|
|
@ -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 = $( '<div id="'+pane_name1+'" class="split split-'+direction+'" />' )
|
|
||||||
var first_sub = $( '<div id="'+pane_name1+'-sub" class="split-sub" />' )
|
|
||||||
var second_div = $( '<div id="'+pane_name2+'" class="split split-'+direction+'" />' )
|
|
||||||
var second_sub = $( '<div id="'+pane_name2+'-sub" class="split-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 = $( '<div id="'+pane1_parent.attr('id')+'-sub" class="split-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,
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
@ -5,625 +5,285 @@
|
||||||
* This is used in conjunction with the main evennia.js library, which
|
* This is used in conjunction with the main evennia.js library, which
|
||||||
* handles all the communication with the Server.
|
* handles all the communication with the Server.
|
||||||
*
|
*
|
||||||
* The job of this code is to create listeners to subscribe to evennia
|
* The job of this code is to coordinate between listeners subscribed to
|
||||||
* messages, via Evennia.emitter.on(cmdname, listener) and to handle
|
* evennia messages and any registered plugins that want to process those
|
||||||
* input from the user and send it to
|
* messages and send data back to Evennia
|
||||||
* Evennia.msg(cmdname, args, kwargs, [callback]).
|
*
|
||||||
|
* 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 plugins = {}; // Global plugin objects by name.
|
||||||
var input_history = function() {
|
// Each must have an init() 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// GUI Event Handlers
|
// Global plugin_handler
|
||||||
//
|
//
|
||||||
|
var plugin_handler = (function () {
|
||||||
|
"use strict"
|
||||||
|
|
||||||
// Grab text from inputline and send to Evennia
|
var ordered_plugins = new Array; // plugins in <html> loaded order
|
||||||
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], {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Opens the options dialog
|
//
|
||||||
function doOpenOptions() {
|
// Plugin Support Functions
|
||||||
if (!Evennia.isConnected()) {
|
//
|
||||||
alert("You need to be connected.");
|
|
||||||
return;
|
// 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) {
|
// GUI Event Handlers
|
||||||
var dialog = $(event.target).closest(".dialog");
|
//
|
||||||
dialog.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
// catch all keyboard input, handle special chars
|
// catch all keyboard input, handle special chars
|
||||||
function onKeydown (event) {
|
var onKeydown = function (event) {
|
||||||
var code = event.which;
|
// cycle through each plugin's keydown
|
||||||
var history_entry = null;
|
for( let n=0; n < ordered_plugins.length; n++ ) {
|
||||||
var inputfield = $("#inputfield");
|
let plugin = ordered_plugins[n];
|
||||||
if (code === 9) {
|
// does this plugin handle keydown events?
|
||||||
return;
|
if( 'onKeydown' in plugin ) {
|
||||||
}
|
// yes, does this plugin claim this event exclusively?
|
||||||
|
if( plugin.onKeydown(event) ) {
|
||||||
//inputfield.focus();
|
// 'true' claims this event has been handled
|
||||||
|
return;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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("<div class='" + cls + "'>" + args[0] + "</div>");
|
|
||||||
var scrollHeight = mwin.parent().parent().prop("scrollHeight");
|
|
||||||
mwin.parent().parent().animate({ scrollTop: scrollHeight }, 0);
|
|
||||||
|
|
||||||
onNewLine(args[0], null);
|
// 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 () {
|
||||||
// Handle prompt output from the server
|
// cycle through each plugin to look for unload handlers
|
||||||
function onPrompt(args, kwargs) {
|
for( let n=0; n < ordered_plugins.length; n++ ) {
|
||||||
// show prompt
|
let plugin = ordered_plugins[n];
|
||||||
$('#prompt')
|
if( 'onBeforeUnload' in plugin ) {
|
||||||
.addClass("out")
|
plugin.onBeforeUnload();
|
||||||
.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(
|
|
||||||
"<div class='msg err'>"
|
|
||||||
+ "Error or Unhandled event:<br>"
|
|
||||||
+ cmdname + ", "
|
|
||||||
+ JSON.stringify(args) + ", "
|
|
||||||
+ JSON.stringify(kwargs) + "<p></div>");
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
|
||||||
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");
|
// Evennia Public Event Handlers
|
||||||
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 == "" ) {
|
// Handle onLoggedIn from the server
|
||||||
new_pane1 = 'pane_'+num_splits;
|
var onLoggedIn = function (args, kwargs) {
|
||||||
num_splits++;
|
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;
|
// Handle onGotOptions from the server
|
||||||
num_splits++;
|
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');
|
// Handle text coming from the server
|
||||||
return;
|
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');
|
// Handle prompt output from the server
|
||||||
return;
|
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");
|
// Handle unrecognized commands from server
|
||||||
}
|
var onDefault = function (cmdname, args, kwargs) {
|
||||||
|
// does this plugin handle this UnknownCmd?
|
||||||
function onSplitDialog() {
|
for( let n=0; n < ordered_plugins.length; n++ ) {
|
||||||
var dialog = $("#splitdialogcontent");
|
let plugin = ordered_plugins[n];
|
||||||
dialog.empty();
|
if( 'onUnknownCmd' in plugin ) {
|
||||||
|
if( plugin.onUnknownCmd(args, kwargs) ) {
|
||||||
dialog.append("<h3>Split?</h3>");
|
// True -- means this plugin claims this UnknownCmd exclusively.
|
||||||
dialog.append('<input type="radio" name="direction" value="vertical" checked> top/bottom<br />');
|
return;
|
||||||
dialog.append('<input type="radio" name="direction" value="horizontal"> side-by-side<br />');
|
}
|
||||||
|
}
|
||||||
dialog.append("<h3>Split Which Pane?</h3>");
|
}
|
||||||
for ( var pane in SplitHandler.split_panes ) {
|
console.log('NO plugin handled this Unknown Evennia Command');
|
||||||
dialog.append('<input type="radio" name="pane" value="'+ pane +'">'+ pane +'<br />');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.append("<h3>New Pane Names</h3>");
|
|
||||||
dialog.append('<input type="text" name="new_pane1" value="" />');
|
|
||||||
dialog.append('<input type="text" name="new_pane2" value="" />');
|
|
||||||
|
|
||||||
dialog.append("<h3>New First Pane</h3>");
|
// Handle the server connection closing
|
||||||
dialog.append('<input type="radio" name="flow1" value="append" checked>append new incoming messages<br />');
|
var onConnectionClose = function (args, kwargs) {
|
||||||
dialog.append('<input type="radio" name="flow1" value="replace">replace old messages with new ones<br />');
|
// 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("<h3>New Second Pane</h3>");
|
onText(["The connection was closed or lost."], {'cls': 'err'});
|
||||||
dialog.append('<input type="radio" name="flow2" value="append" checked>append new incoming messages<br />');
|
|
||||||
dialog.append('<input type="radio" name="flow2" value="replace">replace old messages with new ones<br />');
|
|
||||||
|
|
||||||
dialog.append('<div id="splitclose" class="button">Split It</div>');
|
|
||||||
|
|
||||||
$("#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("<h3>Set Which Pane?</h3>");
|
|
||||||
for ( var pane in SplitHandler.split_panes ) {
|
|
||||||
dialog.append('<input type="radio" name="pane" value="'+ pane +'">'+ pane +'<br />');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.append("<h3>Which content types?</h3>");
|
|
||||||
for ( var type in known_types ) {
|
// Silences events we don't do anything with.
|
||||||
dialog.append('<input type="checkbox" value="'+ known_types[type] +'">'+ known_types[type] +'<br />');
|
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", <enter>, 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('<div id="paneclose" class="button">Make It So</div>');
|
|
||||||
|
|
||||||
$("#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
|
// Event when client finishes loading
|
||||||
$(document).ready(function() {
|
$(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
|
// This is safe to call, it will always only
|
||||||
// initialize once.
|
// initialize once.
|
||||||
Evennia.init();
|
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
|
// register listeners
|
||||||
$("#inputsend").bind("click", doSendText);
|
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)
|
// 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);
|
$(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,
|
// set an idle timer to send idle every 3 minutes,
|
||||||
// to avoid proxy servers timing out on us
|
// to avoid proxy servers timing out on us
|
||||||
setInterval(function() {
|
setInterval( function() { // Connect to server
|
||||||
// Connect to server
|
Evennia.msg("text", ["idle"], {});
|
||||||
Evennia.msg("text", ["idle"], {});
|
},
|
||||||
},
|
60000*3
|
||||||
60000*3
|
|
||||||
);
|
);
|
||||||
console.log("Completed GUI setup");
|
|
||||||
|
|
||||||
|
// Initialize all plugins
|
||||||
|
plugin_handler.init();
|
||||||
|
|
||||||
|
console.log("Completed Webclient setup");
|
||||||
});
|
});
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
|
||||||
|
|
@ -63,15 +63,21 @@ JQuery available.
|
||||||
</script>
|
</script>
|
||||||
<script src={% static "webclient/js/evennia.js" %} language="javascript" type="text/javascript" charset="utf-8"/></script>
|
<script src={% static "webclient/js/evennia.js" %} language="javascript" type="text/javascript" charset="utf-8"/></script>
|
||||||
|
|
||||||
|
|
||||||
<!-- set up splits before loading the GUI -->
|
<!-- set up splits before loading the GUI -->
|
||||||
<script src="https://unpkg.com/split.js/split.min.js"></script>
|
<script src="https://unpkg.com/split.js/split.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.3.0/mustache.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.3.0/mustache.min.js"></script>
|
||||||
<script src={% static "webclient/js/splithandler.js" %} language="javascript"></script>
|
|
||||||
|
|
||||||
<!-- Load gui library -->
|
<!-- Load gui library -->
|
||||||
{% block guilib_import %}
|
{% block guilib_import %}
|
||||||
<script src={% static "webclient/js/webclient_gui.js" %} language="javascript" type="text/javascript" charset="utf-8"></script>
|
<script src={% static "webclient/js/webclient_gui.js" %} language="javascript" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src={% static "webclient/js/plugins/popups.js" %} language="javascript" type="text/javascript"></script>
|
||||||
|
<script src={% static "webclient/js/plugins/options.js" %} language="javascript" type="text/javascript"></script>
|
||||||
|
<script src={% static "webclient/js/plugins/history.js" %} language="javascript" type="text/javascript"></script>
|
||||||
|
<script src={% static "webclient/js/plugins/default_in.js" %} language="javascript" type="text/javascript"></script>
|
||||||
|
<script src={% static "webclient/js/plugins/oob.js" %} language="javascript" type="text/javascript"></script>
|
||||||
|
<script src={% static "webclient/js/plugins/notifications.js" %} language="javascript" type="text/javascript"></script>
|
||||||
|
<script src={% static "webclient/js/plugins/splithandler.js" %} language="javascript" type="text/javascript"></script>
|
||||||
|
<script src={% static "webclient/js/plugins/default_out.js" %} language="javascript" type="text/javascript"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
<script src="https://cdn.rawgit.com/ejci/favico.js/master/favico-0.3.10.min.js" language="javascript" type="text/javascript" charset="utf-8"></script>
|
<script src="https://cdn.rawgit.com/ejci/favico.js/master/favico-0.3.10.min.js" language="javascript" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
|
||||||
|
|
@ -6,67 +6,19 @@
|
||||||
- guilib_import - for using your own gui lib
|
- guilib_import - for using your own gui lib
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
{% block client %}
|
{% block client %}
|
||||||
<div id="toolbar">
|
|
||||||
<button id="optionsbutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">⚙<span class="sr-only sr-only-focusable">Settings</span></button>
|
|
||||||
<button id="splitbutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">⇹<span class="sr-only sr-only-focusable">Splits</span></button>
|
|
||||||
<button id="panebutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">⚙<span class="sr-only sr-only-focusable">Splits</span></button>
|
|
||||||
<button id="undobutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">↶<span class="sr-only sr-only-focusable">Splits</span></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- The "Main" Content -->
|
<!-- Basic toolbar -->
|
||||||
|
<div id="toolbar"></div>
|
||||||
|
|
||||||
|
<!-- "Main" Content -->
|
||||||
<div id="main" class="split split-vertical" data-role-default>
|
<div id="main" class="split split-vertical" data-role-default>
|
||||||
<div id="main-sub" class="split-sub">
|
<div id="main-sub" class="split-sub">
|
||||||
<div id="messagewindow"></div>
|
<div id="messagewindow"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- The "Input" Pane -->
|
<!-- "Input" Pane -->
|
||||||
<div id="input" class="split split-vertical" data-role-input data-update-append></div>
|
<div id="input" class="split split-vertical" data-role-input data-update-append>
|
||||||
|
|
||||||
<!-- Basic UI Components -->
|
|
||||||
<div id="splitdialog" class="dialog">
|
|
||||||
<div class="dialogtitle">Split Pane<span class="dialogclose">×</span></div>
|
|
||||||
<div class="dialogcontentparent">
|
|
||||||
<div id="splitdialogcontent" class="dialogcontent">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="optionsdialog" class="dialog">
|
|
||||||
<div class="dialogtitle">Options<span class="dialogclose">×</span></div>
|
|
||||||
<div class="dialogcontentparent">
|
|
||||||
<div class="dialogcontent">
|
|
||||||
<h3>Output display</h3>
|
|
||||||
<label><input type="checkbox" data-setting="gagprompt" value="value">Don't echo prompts to the main text area</label><br />
|
|
||||||
<label><input type="checkbox" data-setting="helppopup" value="value">Open help in popup window</label><br />
|
|
||||||
<hr />
|
|
||||||
<h3>Notifications</h3>
|
|
||||||
<label><input type="checkbox" data-setting="notification_popup" value="value">Popup notification</label><br />
|
|
||||||
<label><input type="checkbox" data-setting="notification_sound" value="value">Play a sound</label><br />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="helpdialog" class="dialog">
|
|
||||||
<div class="dialogtitle">Help<span class="dialogclose">×</span></div>
|
|
||||||
<div class="dialogcontentparent">
|
|
||||||
<div class="dialogcontent">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="text/html" id="split-template">
|
|
||||||
<div class="split content<%#horizontal%> split-horizontal<%/horizontal%>" id='<%id%>'>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script type="text/html" id="output-template">
|
|
||||||
<div id="<%id%>" role="log" data-role-output data-update-append data-tags='[<%#tags%>"<%.%>", <%/tags%>]'></div>
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script type="text/html" id="input-template">
|
|
||||||
<div id="inputform" class="wrapper">
|
<div id="inputform" class="wrapper">
|
||||||
<div id="prompt" class="prompt">
|
<div id="prompt" class="prompt">
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -77,7 +29,7 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue