/*
Copyright (c) 2013-2017,2020 D. "Orville" Brodbeck.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// subclient.js -- Defines a bare-bones client object for trigger-spawned windows
// SubClient - creates new SubClient object.
//
// Params: win windowId of the UI
// world world object from the parent (used for settings)
// onSend function to call to send text.
// worldType detected type of world, used for auto-escape
function SubClient(win,world,winName,onSend,worldType) {
var parent=this;
parent.onSend=onSend;
parent.openUrlList=settings.local.openUrlList;
parent.AutoPreview=world.AutoPreview;
// Initialize UI elements and local variables:
var autoEscapeMode=false;
var initAlreadyDone=false;
var focused=true;
var firstNotification=true;
var notifications=Array();
parent.activity=false;
parent.winId=win;
parent.winName=winName;
var timestamps=false;
if (settings.prefs.timestamps) {
timestamps=true;
parent.winId.contentWindow.jss(".timestamp").set("display","table-cell");
} else {
parent.winId.contentWindow.jss(".timestamp").set("display","none");
}
var $d=$(parent.winId.contentWindow.document);
var obsFontSize=new PathObserver(settings,'local.fontsize');
obsFontSize.open(function(newValue,oldValue) {
if (newValue) {
_changeFontSize(newValue);
}
});
var obsFontFamily=new PathObserver(settings,'local.font');
obsFontFamily.open(function(newValue,oldValue) {
_changeFontFamily(newValue);
});
var resizing=false; // Flag for whether the user is resizing the window.
var scrollpause; // Whether or not we've paused scroll-to-bottom-on-output
var commandBuffer=Array(); // Array to hold previous commands for retrieval.
var commandbufferpos; // Position in the command buffer.
// Create world-specific UI elements:
var $div=$d.find("#divWorld");
// Wrapper div
var $divWorld=$('
').appendTo($div);
// Output area
var $divOutput=$('').appendTo($divWorld);
var output=new Output($divOutput);
// Input area
var $divInput=$('').appendTo($divWorld);
var $divUpDown=$('').appendTo($divInput);
var $btnUp=$(' ').appendTo($divUpDown);
var $btnDown=$('').appendTo($divUpDown);
$divUpDown.attr("title","Recall previous/next command. (Ctrl-Up, Ctrl-Down)");
var $txtCommand=$('').appendTo($divInput);
parent.$txtCommand=$txtCommand; // Main routine has to be able to focus our input box.
var $btnSend=$('').appendTo($divInput);
$btnSend.css("float","right").css("height","52px");
$btnSend.css("border-style","solid").css("border-color","black");
$btnSend.attr("title","Send text to server. (Enter)");
$btnSend.button();
// World status/icon bar
var $divMenubar=$('').appendTo($divWorld);
var $divSSL=$('').appendTo($divMenubar);
var $btnSSL=$('').appendTo($divSSL);
var $divStatus=$('').appendTo($divSSL);
var throbber=new ConnectThrobber($divStatus);
var $divIcons=$('').appendTo($divMenubar);
var $btnUrlList=$('\uF143').appendTo($divIcons);
// var $btnPop=$('\uF01B').appendTo($divIcons);
// $btnPop.attr("title","Pop out to new window");
// $btnPop.click(_popOut);
var $btnEnlarge=$('\uF098').appendTo($divIcons);
$btnEnlarge.attr("title","Make text bigger");
var $btnReduce=$('\uF099').appendTo($divIcons);
$btnReduce.attr("title","Make text smaller");
var $btnSearch=$('\uF094').appendTo($divIcons);
var $btnPaused=$('\uF16C').appendTo($divIcons);
$btnPaused.attr("title","Click to resume auto-scrolling");
$btnPaused.hide();
var $btnTimestamps=$('schedule').appendTo($divIcons);
// Title tab
if (quirks.haveWindowTitles) {
// Slide menu bar up to fill the gap where the tab would be.
$divMenubar.css("top","0px");
} else {
$d.find("#titletab").text($d.get(0).title);
$divMenubar.css("top",$d.find("#titlebar").height()+"px");
}
$divOutput.css("top",(parseInt($divMenubar.position().top)+parseInt($divMenubar.height()))+"px");
$d.find("#btnClose").prop("title","Close");
// URL list:
var $divUrlHeader=$('');
$('URL List').appendTo($divUrlHeader);
var $btnCloseUrlList=$('').appendTo($divUrlHeader);
$divUrlHeader.appendTo($divWorld);
var $divUrlListContainer=$('').appendTo($divWorld);
var $divUrlList=$('').appendTo($divUrlListContainer);
parent.divUrlList=$divUrlList;
$divUrlHeader.css("top",$divMenubar.offset().top+$divMenubar.height());
$divUrlListContainer.css("top",$divUrlHeader.offset().top+$divUrlHeader.height());
$divUrlListContainer.hide();
$divUrlHeader.hide();
_initResizable();
// Search bar:
var $divSearchBar=$('').appendTo($divWorld);
$divSearchBar.hide();
var $divSearch=$('').appendTo($divSearchBar);
var $btnCloseSearch=$('').prependTo($divSearch);
var $txtSearch=$('').appendTo($divSearch);
var $divSearchInfo=$('').appendTo($divSearch);
var $btnFindPrevious=$('').appendTo($divSearch);
var $btnFindNext=$('').appendTo($divSearch);
if (worldType == "MUSH") {
_initAutoEscape();
}
_resizeAll();
commandBuffer[0]="";
commandbufferpos=0;
output.setFontSize(settings.local.fontsize);
$txtCommand.css('font-size',settings.local.fontsize+"px");
// Set up various event handlers:
_setEventHandlers(true);
chrome.notifications.onClicked.addListener(_notificationClick);
// == Public method definitions start below ==
// Add text to the output window.
parent.htmlAppend = htmlAppend;
function htmlAppend(html) {
output.htmlAppend(html);
_activityNotification($('').html($(html).find(".outputlinetext:first")).text());
}
// Yank back text. Used when we get the second half of a partial line
parent.yankLast = yankLast;
function yankLast() {
output.yankLast();
}
// Add error text to the output window.
parent.error = error;
function error(text) {
output.error(text);
}
// Add warning text to the output window.
parent.warning = warning;
function warning(text) {
output.warning(text);
}
// Change input prefix
parent.setInputPrefix=setInputPrefix;
function setInputPrefix(prefix) {
if (parent.inputPrefix == prefix) {
// Degenerate case -- the prefix is already set to our argument.
return;
}
var oldprefix=parent.inputPrefix;
parent.inputPrefix=prefix;
// Update text box, but only if the user hasn't entered something
if (($txtCommand.val() == oldprefix) || ($txtCommand.val() === "")) {
$txtCommand.val(prefix);
}
}
// Add a URL to the URL list.
parent.urlListAdd=urlListAdd;
function urlListAdd(linkdest, linktext) {
// Turn off autopreview for Chrome versions before 49, due to bug #547023
if (quirks.browserVersion < 49) {
if (parent.AutoPreview) {
console.warn("Disabling URL autopreview due to crbug #547023.");
}
parent.AutoPreview=false;
}
if (parent.AutoPreview === undefined) {
parent.AutoPreview=false;
}
var $newdiv=$('');
var $urltext=$('
').prependTo($newdiv);
if (parent.AutoPreview) {
_generatePreview(linkdest,$newdiv);
} else if (parent.winId.contentWindow !== undefined) {
var $placeholder=$('').prependTo($newdiv);
$placeholder.click(function() {
var linkdest=$(this).prop("id");
var $divCurrent=$(this).parents("div.urllistitem");
$(this).remove();
_generatePreview(linkdest,$divCurrent);
});
}
var $urldiv=$newdiv.appendTo($divUrlList);
$divUrlList.scrollTop($divUrlList[0].scrollHeight);
}
// Handle key events inside the input box. This is public so we can pass through an
// event from the main routine that should be handled here.
parent.inputBoxKeypress = inputBoxKeypress;
function inputBoxKeypress(event) {
// Handle keypresses inside the input box.
if (event.which == 13) { // ENTER
event.preventDefault();
_sendCommand();
} else if ((event.which == 8 || event.which == 87) && event.ctrlKey) { // Ctrl-Backspace
event.preventDefault();
var regexpLastWord=/(^|.*\s+)[^\s]+\s*$/;
var caret=_doGetCaretPosition($txtCommand[0]);
var leftofcursor=$txtCommand.val().substring(0,caret-1);
var rightofcursor=$txtCommand.val().substring(caret);
var matches=regexpLastWord.exec( leftofcursor );
if (matches) {
$txtCommand.val(matches[1]+rightofcursor);
_doSetCaretPosition($txtCommand[0],matches[1].length);
}
commandBuffer[commandBuffer.length-1]=$txtCommand.val();
} else if (event.which == 85 && event.ctrlKey) { // Ctrl-U
event.preventDefault();
$txtCommand.val(parent.inputPrefix);
} else if ( (event.which == 38 && event.ctrlKey) || // Ctrl-Up
(event.which == 80 && event.ctrlKey) ) { // Ctrl-P
event.preventDefault();
_previousCommand();
} else if ( (event.which == 40 && event.ctrlKey) || // Ctrl-Down
(event.which == 78 && event.ctrlKey) ) { // Ctrl-N
event.preventDefault();
_nextCommand();
} else if ( event.which == 70 && (event.ctrlKey || event.metaKey)) { // Ctrl-F or Command-F
event.preventDefault();
if ($divSearchBar.is(":visible")) {
_searchHide();
} else {
_searchShow();
}
} else {
// Update the command buffer.
if (commandbufferpos == (commandBuffer.length-1)) {
commandBuffer[commandBuffer.length-1]=$txtCommand.val();
}
}
_resizeInputBox();
} // inputBoxKeypress
// move() - Move the UI to a new window. Used when relaunching after our window has been closed.
parent.move=move;
function move(winId) {
if (typeof parent.winId.contentWindow.initResizable == "function") {
parent.winId.contentWindow.initResizable($divUrlListContainer.get(0),"destroy");
}
parent.winId=winId;
var doc=winId.contentWindow.document;
$d=$(doc);
$($div).appendTo(doc.body);
if (quirks.haveWindowTitles) {
$divMenubar.css("top","0px");
} else {
$(doc).find("#titletab").text($d.get(0).title);
$divMenubar.css("top",$(doc).find("#titlebar").height()+"px");
}
$divOutput.css("top",(parseInt($divMenubar.position().top)+parseInt($divMenubar.height()))+"px");
$d=$(doc);
_initResizable();
_setEventHandlers();
_setTimestamps();
_resizeAll();
}
// close() - Permanently destroy the subclient, removing all its event handlers, so it can be gc'd.
parent.close=close;
function close() {
parent.onSend=null;
chrome.notifications.onClicked.removeListener(_notificationClick);
// The contentWindow object may not exist if the window has been closed.
if (typeof parent.winId.contentWindow.removeEventListener == "function") {
parent.winId.contentWindow.removeEventListener("resize",_resizeAll);
parent.winId.contentWindow.removeEventListener("focus",_onFocus);
parent.winId.contentWindow.removeEventListener("blur",_onBlur);
parent.winId.contentWindow.initResizable($divUrlListContainer.get(0),"destroy");
}
$d.off("keydown");
$txtCommand.off("keydown");
$divOutput.off("scroll");
$btnSend.off("click");
$btnUp.off("click");
$btnDown.off("click");
$btnSearch.off("click");
$btnCloseSearch.off("click");
$txtSearch.off("keydown");
$txtSearch.off("keyup");
$btnFindPrevious.off("click",_searchPrevious);
$btnFindNext.off("click",_searchNext);
$btnUrlList.off("click");
$btnEnlarge.off("click");
$btnReduce.off("click");
$btnPaused.off("click");
$btnCloseUrlList.off("click");
if ($chkAutoEscape) {
$chkAutoEscape.off("click",_autoEscapeToggle);
}
obsFontSize.close();
obsFontFamily.close();
obsFontSize=null;
obsFontFamily=null;
}
// == Public methods end here -- below are private functions. ==
// Initialize the resizable elements in the client window.
function _initResizable() {
var resizeOptions={
handles: "e",
alsoResize: $divUrlHeader.get(0),
minWidth: 100,
resize: _resizeAll
};
if (parent.winId.contentWindow !== undefined) {
parent.winId.contentWindow.initResizable($divUrlListContainer.get(0),resizeOptions);
} else {
parent.winId.initResizable($divUrlListContainer.get(0),resizeOptions);
}
if (!parent.openUrlList) {
if ($divUrlListContainer.is(":visible")) {
_urlListHide();
}
} else {
_urlListShow();
}
}
// Set up our window event handlers. If "init" does not equal true,
// we try to clear the old ones first, assuming we're moving from
// one window to another.
function _setEventHandlers(init) {
parent.winId.contentWindow.addEventListener("resize",_resizeAll);
parent.winId.contentWindow.addEventListener("focus",_onFocus);
parent.winId.contentWindow.addEventListener("blur",_onBlur);
if (!init) {
$d.off("keydown");
$txtCommand.off("keydown");
$divOutput.off("scroll");
$btnSend.off("click");
$btnUp.off("click");
$btnDown.off("click");
$btnSearch.off("click");
$btnCloseSearch.off("click");
$txtSearch.off("keydown");
$txtSearch.off("keyup");
$btnFindPrevious.off("click",_searchPrevious);
$btnFindNext.off("click",_searchNext);
$btnUrlList.off("click");
$btnEnlarge.off("click");
$btnReduce.off("click");
$btnPaused.off("click");
$btnCloseUrlList.off("click");
$btnTimestamps.off("click",_toggleTimestamps);
if ($chkAutoEscape) {
$chkAutoEscape.off("click",_autoEscapeToggle);
}
}
$d.on("keydown",_onKey);
$btnEnlarge.on("click",_enlargeFont);
$btnReduce.on("click",_reduceFont);
$btnPaused.on("click",_outputUnpause);
$divOutput.on('scroll',_outputScroll);
$btnSend.on("click",_sendCommand);
$btnFindPrevious.on("click",_searchPrevious);
$btnFindNext.on("click",_searchNext);
if ($chkAutoEscape) {
$chkAutoEscape.on("click",_autoEscapeToggle);
}
// Command buffer
$txtCommand.on("keydown",inputBoxKeypress);
$btnUp.on("click",_previousCommand);
$btnDown.on("click",_nextCommand);
// Search
$btnSearch.on("click",function(){
if ($divSearchBar.is(":visible")) {
_searchHide();
} else {
_searchShow();
}
});
$btnCloseSearch.on("click",_searchHide);
$txtSearch.on("keydown",function(event) {
if (event.which==70 && (event.ctrlKey || event.metaKey)) {
// Prevent browser search bar from opening in tab mode.
event.preventDefault();
}
});
$txtSearch.on("keyup",function(event){
_searchUpdate();
});
$btnUrlList.on("click",function() {
if ($divUrlListContainer.is(":visible")) {
_urlListHide();
} else {
_urlListShow();
}
});
$btnCloseUrlList.on("click",_urlListHide);
$btnTimestamps.on("click",_toggleTimestamps);
} // setEventHandlers()
// Handle keyboard commands.
function _onKey(event) {
switch(event.which) {
case 186:
case 222: // " or : outside an input element? Focus the input box.
if (event.shiftKey && (event.target.tagName != "INPUT")) {
$(client[activeworld].$txtCommand).focus();
}
break;
default:
if (event.which == 87 && event.ctrlKey) {
event.preventDefault();
}
if (event.target.tagName != "INPUT" && event.target.tagName != "TEXTAREA") {
if (event.which >= 65 && event.which <= 90 && !event.altKey && !event.metaKey
&& !event.ctrlKey) {
// Redirect typing that's outside an input element to the input box.
// We avoid anything with bucky bits set so as not to interfere with
// default keybindings.
$txtCommand.focus();
}
} else if (event.which == 86 && (event.ctrlKey || event.metaKey)) {
// Paste events should happen inside the input box.
$txtCommand.focus();
}
}
return;
}
function _onFocus() {
focused=true;
firstNotification=true;
parent.activity=false;
// Clear all outstanding activity notifications.
var notification;
while (notification=notifications.pop()) {
chrome.notifications.clear(notification,function(wasCleared) {
// We do nothing in the callback, for now.
});
}
$txtCommand.focus();
}
function _onBlur() {
focused=false;
}
// Accepts a URL, and creates a preview thumbnail image of it.
// This is then prepended into $div.
function _generatePreview(url,$div) {
var webview=document.createElement("webview");
$(webview).attr("partition","DuckClient");
var $webview=$(webview).prependTo($div);
var title=null;
var invalidImageSize=100;
if (parent.AutoPreview) {
// Make invalid image icon less obtrusive if autopreview is on.
invalidImageSize=32;
}
// Echo the webview's console to the main console, for easier debugging .
webview.addEventListener('consolemessage', function(e) {
console.log('Image preview: ', e.message,'@',e.sourceId+":"+e.line);
});
webview.addEventListener('permissionrequest',function(e) {
// Deny requests to load plugins, etc.
e.request.deny();
});
webview.addEventListener("loadstart",function(e) {
if (!$(webview).prop("injected")) {
// Inject a script to kill any audio or video playback.
webview.executeScript( {file: "mediakiller.js"} );
// For various reasons the page load can hang up and cause the loadStop event to
// never fire. We set a timer so we don't leave the webview hanging around.
window.setTimeout(function() {
if ($(webview)) {
console.log("Timeout: Killing webview.");
var $urltext=$(webview).parent().find(".urltext");
var height=$urltext.parent().height();
webview.stop();
var $img=$('');
$webview.replaceWith($img);
if ($urltext.height() > height) {
height=$urltext.height();
}
if (invalidImageSize > height) {
height=invalidImageSize;
}
$urltext.parent().height(height);
}
},90000);
$(webview).prop("injected",true);
}
});
webview.addEventListener("loadabort",function(e) {
webview.stop();
webview.executeScript( {file: "imagepreview.js"},function(data) {
var $urltext=$(webview).parent().find(".urltext");
// What we get back from the injected script is an image data URL
if (data !== undefined) {
if (data[0] && data[0][0] !== undefined) {
if (data[0][0].match(/^reload/)) {
if (data[0][2]) {
title=data[0][2];
}
// We need to load a different page to get the actual image.
// Used by art archive scrapers.
webview.src=data[0][1];
} else {
var $img=$('');
$(webview).replaceWith($img);
$divUrlList.scrollTop($divUrlList[0].scrollHeight);
if (data[0][1] && !title) {
title=data[0][1];
}
if (title && !title.match("404")) {
$urltext.css("word-wrap","normal");
$urltext.css("word-break","normal");
$urltext.html(''+title+' ');
}
}
} else {
var $img=$('');
$(webview).replaceWith($img);
$divUrlList.scrollTop($divUrlList[0].scrollHeight);
if (data[0] && data[0][1] && !title) {
title=data[0][1];
}
if (title) {
$urltext.css("word-wrap","normal");
$urltext.css("word-break","normal");
$urltext.html(''+title+' ');
}
}
}
});
});
webview.addEventListener("loadstop",function() {
webview.stop();
webview.executeScript( {file: "imagepreview.js"},function(data) {
var $urltext=$(webview).parent().find(".urltext");
// What we get back from the injected script is an image data URL
if (data !== undefined) {
if (data[0] && data[0][0]) {
if (data[0][0].match(/^reload/)) {
if (data[0][2]) {
title=data[0][2];
}
// We need to load a different page to get the actual image.
// Used by art archive scrapers.
webview.src=data[0][1];
} else {
var $img=$('');
$(webview).replaceWith($img);
$divUrlList.scrollTop($divUrlList[0].scrollHeight);
if (data[0][1] && !title) {
title=data[0][1];
}
if (title && !title.match("404")) {
$urltext.css("word-wrap","normal");
$urltext.css("word-break","normal");
$urltext.html(''+title+' ');
}
}
} else {
var $img=$('');
$(webview).replaceWith($img);
$divUrlList.scrollTop($divUrlList[0].scrollHeight);
if (data[0] && data[0][1] && !title) {
title=data[0][1];
}
if (title) {
$urltext.css("word-wrap","normal");
$urltext.css("word-break","normal");
$urltext.html(''+title+' ');
}
}
}
});
});
webview.src=url;
} // generatePreview
// Show URL List
function _urlListShow() {
$divUrlListContainer.show();
$divUrlHeader.show();
// Don't mess with the window width if another client already has.
if (!parent.openUrlList) {
parent.openUrlList=true;
// Widen app window if possible, to keep the output width the same.
if (parent.winId.contentWindow !== undefined) {
var urlListWidth=$divUrlListContainer.get(0).offsetWidth;
var screenWidth=parent.winId.contentWindow.screen.availWidth;
var clientWidth=parent.winId.getBounds().width+urlListWidth;
parent.openUrlListDirection="right";
if (clientWidth < screenWidth) {
// We have room to expand...
var clientBounds={};
var clientLeft=parent.winId.getBounds().left;
clientBounds.width=clientWidth;
// Check if we have room to expand to the left.
if (clientLeft > urlListWidth) {
clientBounds.left=clientLeft-urlListWidth;
parent.openUrlListDirection="left";
}
parent.winId.setBounds(clientBounds);
}
}
}
_resizeAll();
saveLocal();
} // _urlListShow
function _urlListHide() {
//$divUrlList.resizable( "disable" );
var urlListWidth=$divUrlListContainer.get(0).offsetWidth;
var clientBounds={};
$divUrlListContainer.hide();
$divUrlHeader.hide();
if (parent.openUrlList && (parent.winId.contentWindow !== undefined)) {
clientBounds.width=parent.winId.getBounds().width-urlListWidth;
if (parent.openUrlListDirection
&& parent.openUrlListDirection.match("left")) {
clientBounds.left=parent.winId.getBounds().left+urlListWidth;
}
parent.winId.setBounds(clientBounds);
}
parent.openUrlList=false;
saveLocal();
_resizeAll();
} // _urlListHide
// Per-client globals for search status.
var searchNumTargets;
var searchCurrentTarget;
// Open search bar
function _searchShow() {
$divSearchBar.show();
// Adjust position, which will vary depending on if we're running
// as a tab or a window.
$divSearchBar.css("top",($divMenubar.get(0).offsetTop+$divMenubar.get(0).offsetHeight)+"px");
$txtSearch.focus();
_searchUpdate();
}
function _searchUpdate() {
output.searchUnmark();
if ($txtSearch.val().length > 2) {
output.searchMark($txtSearch.val());
if (output.numFound === 0) {
$divSearchInfo.text("Not found");
$btnFindNext.attr("disabled",true);
$btnFindPrevious.attr("disabled",true);
} else {
_searchScrollToTarget(output.numFound);
$btnFindNext.attr("disabled",false);
$btnFindPrevious.attr("disabled",false);
}
} else {
$divSearchInfo.text("");
$btnFindNext.attr("disabled",true);
$btnFindPrevious.attr("disabled",true);
}
}
function _searchScrollToTarget(n) {
$divSearchInfo.text(n+" of "+output.numMatches);
output.searchScrollTo(n);
}
function _searchPrevious() {
// Scroll to previous target.
if (output.currentMatch==1) {
_searchScrollToTarget(output.numFound); // Search wrapped.
} else {
_searchScrollToTarget(output.currentMatch-1);
}
}
function _searchNext() {
// Scroll to next target.
if (output.currentMatch==output.numFound) {
_searchScrollToTarget(1); // Search wrapped.
} else {
_searchScrollToTarget(output.currentMatch+1);
}
}
function _searchHide() {
$divSearchBar.hide();
$txtCommand.focus();
output.searchUnmark();
}
// Expand/shrink input box as necessary.
function _resizeInputBox() {
$txtCommand.height(42);
var scrollheight=$txtCommand.get(0).scrollHeight;
var maxheight=Math.floor($(parent.winId.contentWindow.document).height()/3);
if ($txtCommand.height()= 0) {
$txtCommand.val(commandBuffer[commandbufferpos]);
_resizeInputBox();
} else {
commandbufferpos=0;
}
}
function _nextCommand() {
commandbufferpos++;
if (commandbufferpos < commandBuffer.length) {
$txtCommand.val(commandBuffer[commandbufferpos]);
_resizeInputBox();
} else {
commandbufferpos=(commandBuffer.length-1);
}
}
// Get the current cursor position in a text input control. Used for Ctrl-Backspace
// functionality. From http://snipplr.com/view/5144/getset-cursor-in-html-textarea/
function _doGetCaretPosition (ctrl) {
var CaretPos = 0;
// IE Support
if (document.selection) {
ctrl.focus ();
var Sel = document.selection.createRange ();
Sel.moveStart ('character', -ctrl.value.length);
CaretPos = Sel.text.length;
}
// Firefox support
else if (ctrl.selectionStart || ctrl.selectionStart == '0')
CaretPos = ctrl.selectionStart;
return (CaretPos);
}
function _doSetCaretPosition(ctrl, pos)
{
if(ctrl.setSelectionRange)
{
ctrl.focus();
ctrl.setSelectionRange(pos,pos);
}
else if (ctrl.createTextRange) {
var range = ctrl.createTextRange();
range.collapse(true);
range.moveEnd('character', pos);
range.moveStart('character', pos);
range.select();
}
}
// Temporarily disable scrolling to the bottom on output if the user has scrolled back.
function _outputScroll() {
if (($divOutput[0].scrollHeight-$divOutput[0].clientHeight-$divOutput[0].scrollTop) > 20) {
output.scrollPaused = true;
$btnPaused.show();
} else {
output.scrollPaused = false;
$btnPaused.hide();
}
}
// Function to unpause output when the pause icon is clicked.
function _outputUnpause() {
output.scrollPaused=false;
output.scrollToBottom();
}
var $chkAutoEscape;
var $divAutoEscape;
// Initialize auto-escape for MUSH type worlds.
function _initAutoEscape() {
if (!$chkAutoEscape) {
$divAutoEscape=$('
Auto-\\
').appendTo($divIcons);
$chkAutoEscape=$divAutoEscape.find("#chkAutoEscape");
$divAutoEscape.attr("title","Add a backslash in front of characters that the MUSH parser would otherwise not display.");
$chkAutoEscape.on("click",_autoEscapeToggle);
autoEscapeMode = true;
if (world["autoEscapeMode"+winName] !== undefined) {
if (world["autoEscapeMode"+winName] == false) {
autoEscapeMode = false;
$chkAutoEscape.prop("checked",false);
}
}
}
}
function _autoEscapeToggle() {
autoEscapeMode = $chkAutoEscape.prop("checked");
world["autoEscapeMode"+winName] = autoEscapeMode;
saveWorldByIndex(world.Index);
$txtCommand.focus();
}
// Pop out to a new window or, if we're already popped out, pop back in.
function _popOut() {
chrome.app.window.create("empty.html",
{ frame : "none", },
function(winId) {
winId.contentWindow.readyCallback=function() {
console.log("Window ready");
_popOutComplete(winId,$divWorld);
}.bind(parent);
parent.originalWinId=parent.winId;
parent.winId=winId;
windowId.push(winId); // Add our new window to the background page's list.
});
}
function _popOutComplete(winId,div) {
var bounds=parent.originalWinId.getBounds();
parent.winId.setBounds(bounds);
var doc=winId.contentWindow.document;
$(div).appendTo(doc.body);
_setEventHandlers();
parent.mainWin.contentWindow.rmButton(parent.number);
$btnPop.text("\uF018");
$btnPop.attr("title","Pop into main window");
$btnPop.off("click",_popOut);
$btnPop.on("click",_popIn);
$(doc).find('#titletab').text(parent.name+" : "+parent.username);
doc.title=parent.name+" : "+parent.username;
$divMenubar.css("top",$(doc).find("#titlebar").height()+"px");
$divOutput.css("top",(parseInt($divMenubar.position().top)+parseInt($divMenubar.height()))+"px");
changeFontSize(parent.winId);
}
function _popIn(e) {
e.stopPropagation();
$divWorld.appendTo($(parent.mainWin.contentWindow.document.body).find("#divWorld"));
var oldWinId=parent.winId;
parent.winId=parent.mainWin;
$btnPop.text("\uF01B").attr("title","Pop out to new window");
$btnPop.off("click",_popIn);
$btnPop.on("click",_popOut);
_setEventHandlers();
parent.mainWin.contentWindow.addButton(parent.number);
oldWinId.contentWindow.close();
$divOutput.css("top","");
$divMenubar.css("top","");
}
// Notify the user that we have activity, if we're not focused.
// If we're also in the main window, bubble the notification up to the
// main window's callback, too.
function _activityNotification(text) {
if (!focused && text.length) {
parent.activity=true;
if (settings.prefs.audioNotifications && firstNotification) {
firstNotification=false;
// Because nwjs tends to crash when we put audio tags in subClient
// windows, we borrow the main window's.
//$(parent.winId.contentWindow.document).find("#audioAlert").get(0).play();
chrome.app.window.get("DuckClientWindow").contentWindow.document.getElementById("audioAlert").play();
}
if (settings.prefs.iconbounce) {
// This sometimes throws an exception on nwjs.
try {
parent.winId.drawAttention();
} catch (err) {
console.warn("Icon bounce failed.");
console.dir(err);
}
}
if (settings.prefs.notifications) {
var notifyOpts={
type: "basic",
title: parent.winId.contentWindow.document.title,
message: text,
iconUrl: "duckclient-128.png"
};
chrome.notifications.create(""+parent.winId.contentWindow.document.title,notifyOpts,function(notificationId){
notifications.push(notificationId);
});
}
}
}
// Callback for when a notification is clicked.
function _notificationClick(notificationId) {
// See if it's one of ours. If so, show our window.
for (var i=0;i