Index: includes/AjaxFunctions.php =================================================================== --- includes/AjaxFunctions.php (revision 16398) +++ includes/AjaxFunctions.php (working copy) @@ -131,4 +131,43 @@ return $response; } +/** + * Called for AJAX watch/unwatch requests. + * @param $pageID Integer ID of the page to be watched/unwatched + * @param $watch String 'w' to watch, 'u' to unwatch + * @return String '' or '' on successful watch or unwatch, respectively, or '' on error (invalid XML in case we want to add HTML sometime) + */ +function wfAjaxWatch($pageID, $watch) { + if(wfReadOnly()) + return ''; // redirect to action=(un)watch, which will display the database lock message + + if(('w' !== $watch && 'u' !== $watch) || !is_numeric($pageID)) + return ''; + $watch = 'w' === $watch; + $pageID = intval($pageID); + + $title = Title::newFromID($pageID); + if(!$title) + return ''; + $article = new Article($title); + $watching = $title->userIsWatching(); + + if($watch) { + if(!$watching) { + $dbw =& wfGetDB(DB_MASTER); + $dbw->begin(); + $article->doWatch(); + $dbw->commit(); + } + } else { + if($watching) { + $dbw =& wfGetDB(DB_MASTER); + $dbw->begin(); + $article->doUnwatch(); + $dbw->commit(); + } + } + + return $watch ? '' : ''; +} ?> Index: includes/DefaultSettings.php =================================================================== --- includes/DefaultSettings.php (revision 16398) +++ includes/DefaultSettings.php (working copy) @@ -2155,6 +2155,13 @@ $wgAjaxSearch = false; /** + * Enable watching/unwatching pages using AJAX. + * Requires $wgUseAjax to be true too. + * Causes wfAjaxWatch to be added to $wgAjaxExportList + */ +$wgAjaxWatch = false; + +/** * List of Ajax-callable functions. * Extensions acting as Ajax callbacks must register here */ Index: includes/OutputPage.php =================================================================== --- includes/OutputPage.php (revision 16398) +++ includes/OutputPage.php (working copy) @@ -480,7 +480,7 @@ function output() { global $wgUser, $wgOutputEncoding, $wgRequest; global $wgContLanguageCode, $wgDebugRedirects, $wgMimeType; - global $wgJsMimeType, $wgStylePath, $wgUseAjax, $wgAjaxSearch, $wgScriptPath, $wgServer; + global $wgJsMimeType, $wgStylePath, $wgUseAjax, $wgAjaxSearch, $wgAjaxWatch, $wgScriptPath, $wgServer; if( $this->mDoNothing ){ return; @@ -491,11 +491,15 @@ if ( $wgUseAjax ) { $this->addScript( "\n" ); - } - if ( $wgUseAjax && $wgAjaxSearch ) { - $this->addScript( "\n" ); - $this->addScript( "\n" ); + if( $wgAjaxSearch ) { + $this->addScript( "\n" ); + $this->addScript( "\n" ); + } + + if( $wgAjaxWatch ) { + $this->addScript( "\n" ); + } } if ( '' != $this->mRedirect ) { Index: includes/Setup.php =================================================================== --- includes/Setup.php (revision 16398) +++ includes/Setup.php (working copy) @@ -169,6 +169,7 @@ $wgPostCommitUpdateList = array(); if ( $wgAjaxSearch ) $wgAjaxExportList[] = 'wfSajaxSearch'; +if ( $wgAjaxWatch ) $wgAjaxExportList[] = 'wfAjaxWatch'; wfSeedRandom(); Index: includes/SkinTemplate.php =================================================================== --- includes/SkinTemplate.php (revision 16398) +++ includes/SkinTemplate.php (working copy) @@ -571,7 +571,7 @@ } $text = wfMsg( $message ); - if ( $text == "<$message>" ) { + if ( wfEmptyMsg( $message, $text ) ) { global $wgContLang; $text = $wgContLang->getFormattedNsText( Namespace::getSubject( $title->getNamespace() ) ); } @@ -1014,10 +1014,35 @@ // by checking for default message content $msgKey = ucfirst($this->skinname).'.js'; $userJS = wfMsg($msgKey); - if ('<'.$msgKey.'>' != $userJS) { + if(!wfEmptyMsg($msgKey, $userJS)) { $s .= $userJS; } + global $wgUseAjax, $wgAjaxWatch; + if($wgUseAjax && $wgAjaxWatch) { + $s .= "\n\n/* AJAX (un)watch (see /skins/common/ajaxwatch.js) */\n"; + $s .= "var wgAjaxWatch = {\n"; + $first = true; + foreach( // array of [name of property in javascript] => [key for wfMsg], [option(s) for wfMsgExt] + array( + 'watchMsg' => array('watch'), + 'unwatchMsg' => array('unwatch'), + 'watchingMsg' => array('watching'), + 'unwatchingMsg' => array('unwatching'), + 'addedWatchMsg' => array('addedwatchajaxtext', 'parse'), + 'removedWatchMsg' => array('removedwatchajaxtext', 'parse') + ) as $jsName => $msg) { + $msgKey = $msg[0]; + array_shift($msg); + if(!$first) + $s .= ",\n"; + else + $first = false; + $s .= "$jsName: '" . str_replace("'", "\\'", str_replace("\n", ' ', wfMsgExt($msgKey, $msg))) . "'"; + } + $s .= "\n};"; + } + wfProfileOut( $fname ); return $s; } Index: languages/MessagesEn.php =================================================================== --- languages/MessagesEn.php (revision 16398) +++ languages/MessagesEn.php (working copy) @@ -1388,11 +1388,15 @@ make it easier to pick out. If you want to remove the page from your watchlist later, click \"Unwatch\" in the sidebar.", +'addedwatchajaxtext' => 'This page has been added to your [[{{ns:special}}:Watchlist|watchlist]].', 'removedwatch' => 'Removed from watchlist', 'removedwatchtext' => "The page \"[[:$1]]\" has been removed from your watchlist.", +'removedwatchajaxtext' => 'This page has been removed from your watchlist.', 'watch' => 'Watch', +'watching' => 'Watching...', 'watchthispage' => 'Watch this page', 'unwatch' => 'Unwatch', +'unwatching' => 'Unwatching...', 'unwatchthispage' => 'Stop watching', 'notanarticle' => 'Not a content page', 'watchnochange' => 'None of your watched items was edited in the time period displayed.', Index: skins/common/ajaxwatch.js =================================================================== --- skins/common/ajaxwatch.js (revision 0) +++ skins/common/ajaxwatch.js (revision 0) @@ -0,0 +1,135 @@ +// dependencies: +// * ajax.js: sajax_init_object(), sajax_do_call() +// * wikibits.js: changeText(), akeyttfor(), hookEvent() + +// wgAjaxWatch should have been initialized in the generated js +if(typeof wgAjaxWatch == "undefined" || !wgAjaxWatch) { + var wgAjaxWatch = { + watchMsg: "Watch", + unwatchMsg: "Unwatch", + watchingMsg: "Watching...", + unwatchingMsg: "Unwatching...", + addedWatchMsg: "This page has been added to your watchlist.", + removedWatchMsg: "This page has been removed from your watchlist." + }; +} + +wgAjaxWatch.supported = true; // supported on current page and by browser +wgAjaxWatch.watching = false; // currently watching page +wgAjaxWatch.inprogress = false; // ajax request in progress +wgAjaxWatch.timeoutID = null; // see wgAjaxWatch.ajaxCall +wgAjaxWatch.watchLink = null; // "watch"/"unwatch" link +wgAjaxWatch.oldHref = null; // url for action=watch/action=unwatch + +wgAjaxWatch.setLinkText = function(newText) { + changeText(wgAjaxWatch.watchLink, newText); +} + +wgAjaxWatch.setLinkID = function(newId) { + wgAjaxWatch.watchLink.id = newId; + akeyttfor(newId); // update tooltip +} + +wgAjaxWatch.ajaxCall = function() { + if(!wgAjaxWatch.supported || wgAjaxWatch.inprogress) + return; + wgAjaxWatch.inprogress = true; + wgAjaxWatch.setLinkText(wgAjaxWatch.watching ? wgAjaxWatch.unwatchingMsg : wgAjaxWatch.watchingMsg); + sajax_do_call("wfAjaxWatch", [wgArticleId, (wgAjaxWatch.watching ? "u" : "w")], wgAjaxWatch.processResult); + // if the request isn't done in 10 seconds, allow user to try again + wgAjaxWatch.timeoutID = window.setTimeout(function() { wgAjaxWatch.inprogress = false; }, 10000); + return; +}; + +wgAjaxWatch.processResult = function(request) { + if(!wgAjaxWatch.supported) + return; + var response = request.responseText; + if(response == "") { + window.location.href = wgAjaxWatch.oldHref; + return; + } else if(response == "") { + wgAjaxWatch.watching = true; + wgAjaxWatch.setLinkText(wgAjaxWatch.unwatchMsg); + wgAjaxWatch.setLinkID("ca-unwatch"); + wgAjaxWatch.oldHref = wgAjaxWatch.oldHref.replace(/action=watch/, "action=unwatch"); + wfDisplayUserMsg("ajaxwatch", wgAjaxWatch.addedWatchMsg); + } else if(response == "") { + wgAjaxWatch.watching = false; + wgAjaxWatch.setLinkText(wgAjaxWatch.watchMsg); + wgAjaxWatch.setLinkID("ca-watch"); + wgAjaxWatch.oldHref = wgAjaxWatch.oldHref.replace(/action=unwatch/, "action=watch"); + wfDisplayUserMsg("ajaxwatch", wgAjaxWatch.removedWatchMsg); + } + wgAjaxWatch.inprogress = false; + if(wgAjaxWatch.timeoutID) + window.clearTimeout(wgAjaxWatch.timeoutID); + return; +}; + +wgAjaxWatch.onLoad = function() { + var x = document.getElementById("ca-unwatch"); + if(x) { + wgAjaxWatch.watching = true; + } else { + wgAjaxWatch.watching = false; + x = document.getElementById("ca-watch"); + if(!x) { + wgAjaxWatch.supported = false; + return; + } + } + + if(!wfSupportsAjax()) { + wgAjaxWatch.supported = false; + return; + } + + wgAjaxWatch.watchLink = x.firstChild; + + wgAjaxWatch.oldHref = wgAjaxWatch.watchLink.getAttribute("href"); + wgAjaxWatch.watchLink.setAttribute("href", "javascript:wgAjaxWatch.ajaxCall()"); + return; +}; + +hookEvent("load", wgAjaxWatch.onLoad); + +/** + * Displays a message to the user. In old browsers, this falls back + * to display an alert() box. + * @param String msgName the name of the message; messages with the + * same name will overwrite each other + * @param String text the content of the message + */ +function wfDisplayUserMsg(msgName, msgText) { + msgName = "msg-" + msgName.replace(/ /g, "-"); // avoid collisions + var msg = document.getElementById(msgName); + if(msg) { + msg.innerHTML = msgText; + return; + } + + var contentsub = document.getElementById("contentSub2"); + if(!contentsub) + contentsub = document.getElementById("contentSub"); + if(!contentsub || !document.createElement) + alert(msgText); + + msg = document.createElement("div"); + msg.id = msgName; + msg.className = "usermessage"; + msg.innerHTML = msgText; + + contentsub.parentNode.insertBefore(msg, contentsub.nextSibling); + return; +} + +/** + * @return boolean whether the browser supports XMLHttpRequest + */ +function wfSupportsAjax() { + var request = sajax_init_object(); + var supportsAjax = request ? true : false; + delete request; + return supportsAjax; +} Index: skins/common/wikibits.js =================================================================== --- skins/common/wikibits.js (revision 16398) +++ skins/common/wikibits.js (working copy) @@ -459,48 +459,78 @@ txtarea.caretPos = document.selection.createRange().duplicate(); } -function akeytt() { - if (typeof ta == "undefined" || !ta) - return; - var pref = 'alt-'; +/** + * @return {String} browser-specific access key prefix + */ +function akeyprefix() { if (is_safari || navigator.userAgent.toLowerCase().indexOf('mac') + 1 || navigator.userAgent.toLowerCase().indexOf('konqueror') + 1 ) - pref = 'control-'; + return 'control-'; if (is_opera) - pref = 'shift-esc-'; + return 'shift-esc-'; + return 'alt-'; +} - for (var id in ta) { - var n = document.getElementById(id); - if (n) { - var a = null; - var ak = ''; - // Are we putting accesskey in it - if (ta[id][0].length > 0) { - // Is this object a object? If not assume it's the next child. +/** + * Sets access key and tooltip for element with specified id. + * @param {String} id id of the element + * @see #akeytt + * @see #akeyttfor2 + */ +function akeyttfor(id) { + akeyttfor2(id, akeyprefix()); +} - if (n.nodeName.toLowerCase() == "a") { - a = n; - } else { - a = n.childNodes[0]; - } +/** + * Same as akeyttfor, but can be more efficient if access key prefix + * is used repeatedly. + * @param {String} id id of the element + * @param {String} akeypref access key prefix + * @see #akeytt + * @see #akeyttfor + */ +function akeyttfor2(id, akeypref) { + if (typeof ta == "undefined" || !ta) + return false; - if (a) { - a.accessKey = ta[id][0]; - ak = ' ['+pref+ta[id][0]+']'; - } - } else { - // We don't care what type the object is when assigning tooltip - a = n; - ak = ''; - } + var n = document.getElementById(id); + if (!n) + return false; - if (a) { - a.title = ta[id][1]+ak; - } + var a = null; + var ak = ''; + // Are we putting access key on it? + if (ta[id][0].length > 0) { + // Is this object a link? If not assume it's the next child. + a = n.nodeName.toLowerCase() == "a" ? n : n.firstChild; + if (a) { + a.accessKey = ta[id][0]; + ak = ' [' + akeypref + ta[id][0] + ']'; } + } else { + // We don't care what type the object is when assigning tooltip + a = n; + ak = ''; } + + if (a) { + a.title = ta[id][1] + ak; + } } +/** + * Sets access keys and tooltips for all elements in the "ta" object. + * @see #akeyttfor + */ +function akeytt() { + if (typeof ta == "undefined" || !ta) + return false; + var pref = akeyprefix(); + for (var id in ta) + akeyttfor2(id, pref); + return true; +} + function setupRightClickEdit() { if (document.getElementsByTagName) { var divs = document.getElementsByTagName('div');