fu: Close-up of Fu, bringing a scoop of water to her mouth (Default)
fu ([personal profile] fu) wrote in [site community profile] changelog2011-03-28 08:25 am

[dw-free] jQuerify comment management (screening, etc)

[commit: http://hg.dwscoalition.org/dw-free/rev/97e54a4c0a87]

http://bugs.dwscoalition.org/show_bug.cgi?id=3536

jQuery version of comment screening, deletion, etc. Includes tweaked
styling, and (reuasble) method of doing the tooltip-like popups for future
use.

Patch by [personal profile] fu.

Files modified:
  • cgi-bin/LJ/S2/EntryPage.pm
  • htdocs/delcomment.bml
  • htdocs/img/ajax-loader.gif
  • htdocs/js/commentmanage.js
  • htdocs/js/jquery.ajaxtip.js
  • htdocs/js/jquery.commentmanage.js
  • htdocs/js/tooltip.dynamic.js
  • htdocs/js/tooltip.dynamic.min.js
  • htdocs/js/tooltip.js
  • htdocs/js/tooltip.min.js
  • htdocs/stc/ajaxtip.css
  • htdocs/stc/popup-form.css
  • htdocs/talkscreen.bml
  • views/dev/tests/commentmanage.html
  • views/dev/tests/commentmanage.js
--------------------------------------------------------------------------------
diff -r 1c0749606568 -r 97e54a4c0a87 cgi-bin/LJ/S2/EntryPage.pm
--- a/cgi-bin/LJ/S2/EntryPage.pm	Mon Mar 28 14:40:04 2011 +0800
+++ b/cgi-bin/LJ/S2/EntryPage.pm	Mon Mar 28 16:25:31 2011 +0800
@@ -372,9 +372,16 @@ sub EntryPage
         $p->{'head_content'} .= $js;
     }
 
-    LJ::need_res(qw(
-                    js/commentmanage.js
-                    ));
+    LJ::need_res( "js/commentmanage.js" );
+    LJ::need_res( { group => "jquery" }, qw(
+            js/jquery/jquery.ui.widget.js
+            js/jquery.ajaxtip.js
+            js/jquery.commentmanage.js
+            js/tooltip.min.js
+            js/tooltip.dynamic.min.js
+            stc/ajaxtip.css
+            stc/popup-form.css
+        ) );
 
     $p->{'_picture_keyword'} = $get->{'prop_picture_keyword'};
 
diff -r 1c0749606568 -r 97e54a4c0a87 htdocs/delcomment.bml
--- a/htdocs/delcomment.bml	Mon Mar 28 14:40:04 2011 +0800
+++ b/htdocs/delcomment.bml	Mon Mar 28 16:25:31 2011 +0800
@@ -20,13 +20,21 @@ _info?><?_code
     use vars qw(%GET %POST);
     use vars qw($body);
 
+    BML::set_language_scope("/delcomment.bml");
+
     my $jsmode = $GET{mode} eq "js";
     $body = "";
 
     my $error = sub {
         if ($jsmode) {
             BML::finish();
-            return "alert('" . LJ::ejs($_[0]) . "'); 0;";
+            # FIXME: remove once we've switched over completely to jquery
+            if ( !!$GET{json} ) {
+                sleep 1 if $LJ::IS_DEV_SERVER;
+                return JSON::objToJson( { error => $_[0] } );
+            } else {
+                return "alert('" . LJ::ejs($_[0]) . "'); 0;";
+            }
         }
         $body = "<?h1 $ML{'Error'} h1?><?p $_[0] p?>";
         return;
@@ -116,7 +124,6 @@ _info?><?_code
     ### perform actions
     if (LJ::did_post() && $POST{'confirm'}) {
         return $error->($ML{'error.invalidform'}) unless LJ::check_form_auth();
-
         # mark this as spam?
         LJ::Talk::mark_comment_as_spam($u, $tp->{talkid})
             if $POST{spam} && $can_spam;
@@ -129,7 +136,6 @@ _info?><?_code
             # delete single comment...
             LJ::Talk::delete_comment($u, $tp->{'itemid'}, $tpid, $tp->{'state'});
         }
-
         # ban the user, if selected
         my $msg;
         if ($POST{'ban'} && $can_ban) {
@@ -138,11 +144,16 @@ _info?><?_code
             $msg = BML::ml('.success.andban', { 'user' => LJ::ljuser($tp->{'userpost'}) });
         }
         $msg ||= $ML{'.success.noban'};
-        $msg .= "<?p $ML{'.success.spam'} p?>" if $POST{spam} && $can_spam;
+        $msg .= " <p>$ML{'.success.spam'}</p>" if $POST{spam} && $can_spam;
 
         if ($jsmode) {
             BML::finish();
-            return "1;";
+            if ( !!$GET{json} ) {
+                sleep 1 if $LJ::IS_DEV_SERVER;
+                return JSON::objToJson( { msg => LJ::strip_html( $msg ) } );
+            } else {
+                return "1;";
+            }
         } else {
             $body = "<?h1 $ML{'.success.head'} h1?><?p $msg p?>";
             return;
diff -r 1c0749606568 -r 97e54a4c0a87 htdocs/img/ajax-loader.gif
Binary file htdocs/img/ajax-loader.gif has changed
diff -r 1c0749606568 -r 97e54a4c0a87 htdocs/js/commentmanage.js
--- a/htdocs/js/commentmanage.js	Mon Mar 28 14:40:04 2011 +0800
+++ b/htdocs/js/commentmanage.js	Mon Mar 28 16:25:31 2011 +0800
@@ -575,7 +575,7 @@ function createModerationFunction (ae, d
             var rpcRes;
 
             if (xtr.status == 200) {
-                var resObj = eval(xtr.responseText);
+                var resObj = eval("resObj = " + xtr.responseText + ";");
                 if (resObj) {
                     poofAt(clickPos);
                     updateLink(ae, resObj, imgTarget);
diff -r 1c0749606568 -r 97e54a4c0a87 htdocs/js/jquery.ajaxtip.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/jquery.ajaxtip.js	Mon Mar 28 16:25:31 2011 +0800
@@ -0,0 +1,114 @@
+(function($) {
+$.widget("dw.ajaxtip", {
+    options: {
+        content: undefined
+    },
+    _namespace: function() {
+        return this.options.namespace ? "."+this.options.namespace : "";
+    },
+    _create: function() {
+        var self = this;
+        var ns = self._namespace();
+
+        var tipcontainer = $("<div class='ajaxtooltip' style='display: none'></div>")
+                        .click(function(e) {e.stopPropagation()})
+
+        self.element
+            .after(tipcontainer)
+            .bind("ajaxresult"+ns, function(e) {
+                self.element.data("tooltip").getTip()
+                     .addClass("ajaxresult-" + e.ajaxresult.status)
+                     .text(e.ajaxresult.message)
+            })
+            .tooltip({
+                predelay: 0,
+                delay: 1500,
+                events: {
+                    def: "ajaxstart"+ns+",ajaxresult"+ns,
+                    tooltip: "mouseover,mouseleave"
+                },
+                position: "bottom center",
+                relative: true,
+                effect: "fade",
+
+                onBeforeShow: function(e) {
+                    var tip = this.getTip();
+                    tip.removeClass("ajaxresult ajaxresult-success ajaxresult-error");
+
+                    if ( self.options.content && ! this.inprogress ){
+                        tip.html(self.options.content)
+                    } else {
+                        tip.empty().append($("<img />", { src: Site.imgprefix + "/ajax-loader.gif" }))
+                            .addClass("ajaxresult")
+                    }
+                }
+            })
+            .dynamic({ classNames: "tip-top tip-right tip-bottom tip-left" });
+    },
+    _init: function() {
+        if(this.options.content)
+            this.element.data("tooltip").show()
+    },
+    widget: function() {
+        return this.element.data("tooltip").getTip();
+    },
+    cancel: function() {
+        var tip = this.element.data("tooltip");
+        if( tip && tip.isShown() ) tip.hide();
+     },
+    load: function(opts) {
+        var self = this;
+
+        var tip = self.element.data("tooltip");
+        if( tip ) {
+            if( tip.inprogress ) return;
+            if( tip.isShown() ) tip.hide();
+        }
+
+        tip.inprogress = true;
+        self.element.trigger("ajaxstart" + self._namespace());
+
+        $.ajax({
+            type: "POST",
+            url : opts.url,
+            data: opts.data,
+
+            dataType: "json",
+            complete: function() {
+                self.element.data("tooltip").inprogress = false;
+            },
+            success: opts.success,
+            error: opts.error
+        });
+    },
+    success: function(msg) {
+        this.element.trigger({ type: "ajaxresult"+this._namespace(),
+                                ajaxresult: { message: msg, status: "success" } })
+    },
+    error: function(msg) {
+        this.element.trigger({ type: "ajaxresult"+this._namespace(),
+                                ajaxresult: { message: msg, status: "error" } })
+    },
+    abort: function(msg) {
+        this.element.data("tooltip").show();
+        this.element.trigger({ type: "ajaxresult"+this._namespace(),
+                                ajaxresult: { message: msg, status: "error" } })
+    }
+});
+
+$.extend( $.dw.ajaxtip, {
+    closeall: function() {
+        $(".ajaxtooltip:visible").each(
+            function(){
+                var tip = $(this).prev().data("tooltip");
+                if ( !tip.inprogress ) tip.hide()
+            })
+    }
+})
+})(jQuery);
+
+jQuery(function($) {
+    $(document).click(function() {
+        $.dw.ajaxtip.closeall();
+    });
+});
diff -r 1c0749606568 -r 97e54a4c0a87 htdocs/js/jquery.commentmanage.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/jquery.commentmanage.js	Mon Mar 28 16:25:31 2011 +0800
@@ -0,0 +1,305 @@
+(function($) {
+$.extractParams = function(url) {
+    if ( ! $.extractParams.cache )
+        $.extractParams.cache = {};
+
+    if ( url in $.extractParams.cache )
+        return $.extractParams.cache[url];
+
+    var search = url.indexOf( "?" );
+    if ( search == -1 ) {
+        $.extractParams.cache[url] = {};
+        return $.extractParams.cache[url];
+    }
+
+    var params = decodeURI( url.substring( search + 1 ) );
+    if ( ! params ) {
+        $.extractParams.cache[url] = {};
+        return $.extractParams.cache[url];
+    }
+
+    var paramsArray = params.split("&");
+    var params = {};
+    for( var i = 0; i < paramsArray.length; i++ ) {
+        var p = paramsArray[i].split("=");
+        var key = p[0];
+        var value = p.length < 2 ? undefined : p[1];
+        params[key] = value;
+    }
+
+    $.extractParams.cache[url] = params;
+    return params;
+};
+
+$.widget("dw.moderate", {
+    options: {
+        journal: undefined,
+        form_auth: undefined,
+
+        endpoint: "__rpc_talkscreen",
+    },
+    _updateLink: function(newData) {
+        this.element.attr("href", newData.newurl);
+
+        var params = $.extractParams(newData.newurl);
+        this.linkdata = {
+            id: params.talkid,
+            action: params.mode,
+            journal: params.journal
+        };
+
+        var image = this.element.find('img[src="'+newData.oldimage+'"]');
+
+        if ( image.length == 0 ) {
+            this.element.text(newData.newalt);
+        } else {
+            image.attr({
+                title: newData.newalt,
+                alt: newData.newalt,
+                src: newData.newimage
+            });
+        }
+    },
+
+    _abort: function(reason, ditemid) {
+        ditemid = ditemid || this.linkdata.id;
+        this.element.ajaxtip({namespace:"moderate"})
+            .ajaxtip( "abort", "Error moderating comment #" + ditemid + ". " + reason);
+    },
+
+    _create: function() {
+        var self = this;
+
+        var params = $.extractParams(this.element.attr("href"));
+        this.linkdata = {
+            id: params.talkid || "",
+            action: params.mode,
+            journal: params.journal
+        };
+
+        this.element.click(function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+
+            if (!self.options.form_auth || ! self.options.journal
+                || !self.linkdata.id || !self.linkdata.action || !self.linkdata.journal) {
+                self._abort( "Not enough context available." );
+                return;
+            }
+            if ( self.linkdata.journal != self.options.journal ) {
+                self._abort( "Journal in link does not match expected journal.");
+                return;
+            }
+            var tomod = $("#cmt" + self.linkdata.id);
+            if( tomod.length == 0 ) {
+                self._abort("Cannot moderate comment which is not visible on this page.");
+                return;
+            }
+
+            var posturl = "/" + self.options.journal + "/" + self.options.endpoint
+                        + "?jsmode=1&json=1&mode=" + self.linkdata.action;
+
+            self.element
+                .ajaxtip({
+                    namespace: "moderate",
+                })
+                .ajaxtip("load", {
+                    url: posturl,
+                    data: {
+                        talkid  : self.linkdata.id,
+                        journal : self.options.journal,
+
+                        confirm : "Y",
+                        lj_form_auth: self.options.form_auth
+                    },
+                    success: function( data, status, jqxhr ) {
+                        if ( data.error ) {
+                            self.element.ajaxtip( "error", "Error while trying to " + self.linkdata.action + ": " + data.error )
+                        } else {
+                            self.element.ajaxtip("success",data.msg);
+                            self._updateLink(data);
+                        }
+                        self._trigger( "complete" );
+                    },
+                    error: function( jqxhr, status, error ) {
+                        self.element.ajaxtip( "error", "Error contacting server. " + error);
+                        self._trigger( "complete" );
+                    }
+                });
+        });
+    }
+});
+
+$.widget("dw.delcomment", {
+    options: {
+        cmtinfo: undefined,
+        journal: undefined,
+        form_auth: undefined,
+
+        endpoint: "__rpc_delcomment"
+    },
+
+    _abort: function(reason, ditemid) {
+        ditemid = ditemid || this.linkdata.id;
+        this.element.ajaxtip({namespace:"delcomment"})
+            .ajaxtip( "abort", "Error deleting comment #" + ditemid + ". " + reason);
+    },
+
+    _create: function() {
+        var self = this;
+
+        var params = $.extractParams(this.element.attr("href"));
+        this.linkdata = {
+            journal: params.journal || "",
+            id: params.id || ""
+        };
+
+        var cmtinfo = self.options.cmtinfo;
+        var cmtdata = cmtinfo ? cmtinfo[this.linkdata.id] : undefined;
+        var remote = cmtinfo ? cmtinfo["remote"] : undefined;
+
+        function deletecomment() {
+            var todel = self.linkdata.id ? $("#cmt" + self.linkdata.id) : [];
+            if( todel.length == 0 ) {
+                self._abort("Comment is not visible on this page.");
+                return;
+            }
+
+            var posturl = "/" + self.options.journal + "/" + self.options.endpoint
+                    +"?"+$.param({ mode: "js", json: 1, journal: self.options.journal, id: self.linkdata.id});
+
+            var postdata = { confirm: 1 };
+            if($("#popdel"+self.linkdata.id+"ban").is(":checked")) postdata["ban"] = 1;
+            if($("#popdel"+self.linkdata.id+"spam").is(":checked")) postdata["spam"] = 1;
+            if($("#popdel"+self.linkdata.id+"thread").is(":checked")) postdata["delthread"] = 1;
+            if(self.options.form_auth) postdata["lj_form_auth"] = self.options.form_auth;
+
+            self.element
+                .ajaxtip("load", {
+                    url: posturl,
+                    data: postdata,
+                    success: function( data, status, jqxhr ) {
+                        if ( data.error ) {
+                            self.element.ajaxtip( "error", "Error while trying to delete comment: " + data.error )
+                        } else {
+                            self.element.ajaxtip("success",data.msg);
+                            removecomment(self.linkdata.id, postdata["delthread"]);
+                        }
+                        self._trigger( "complete" );
+                    },
+                    error: function( jqxhr, status, error ) {
+                        self.element.ajaxtip( "error", "Error contacting server. " + error);
+                        self._trigger( "complete" );
+                    }
+                })
+        }
+
+        function removecomment(ditemid,killchildren) {
+            var todel = $("#cmt" + ditemid);
+            if ( todel.length > 0 ) {
+                todel.fadeOut(2500);
+
+                if ( killchildren ) {
+                    var com = cmtinfo[ditemid];
+                    for ( var i = 0; i < com.rc.length; i++ ) {
+                        removecomment(com.rc[i], true);
+                    }
+                }
+            } else {
+                self._abort( "Child comment is not available on this page", ditemid);
+            }
+        }
+
+        this.element.click(function(e) {
+            e.preventDefault();
+            e.stopPropagation();
+
+            if (!cmtinfo || !remote || !self.options.form_auth || !self.options.journal) {
+                self._abort( "Not enough context available." );
+                return;
+            }
+            if ( !cmtdata ) {
+                self._abort( "Comment is not visible on this page." );
+                return;
+            }
+            if ( self.linkdata.journal != self.options.journal ) {
+                self._abort( "Journal in link does not match expected journal.");
+                return;
+            }
+
+            if ( e.shiftKey ) {
+                self.element.ajaxtip({ namespace: "delcomment" })
+                deletecomment();
+                return;
+            }
+
+            self.element
+                .ajaxtip({
+                    namespace: "delcomment",
+                    content: function() {
+                        var canAdmin = cmtinfo["canAdmin"];
+                        var canSpam = cmtinfo["canSpam"];
+
+                        var form = $("<form class='popup-form'><fieldset><legend>Delete comment?</legend></fieldset></form>");
+                        var ul = $("<ul>").appendTo(form.find("fieldset"));
+
+                        if(remote != "" && cmtdata.u != "" && cmtdata.u != remote && canAdmin) {
+                            var id = "popdel"+self.linkdata.id+"ban";
+                            ul.append($("<li>").append(
+                                $("<input>", { type: "checkbox", value: "ban", id: id}),
+                                $("<label>", { for: id }).html("Ban <strong>"+cmtdata.u+"</strong> from commenting")
+                            ));
+                        }
+
+                        if(remote != "" && cmtdata.u != remote && canSpam) {
+                            var id = "popdel"+self.linkdata.id+"spam";
+                            ul.append($("<li>").append(
+                                $("<input>", { type: "checkbox", value: "spam", id: id}),
+                                $("<label>", { for: id }).text("Mark this comment as spam")
+                            ));
+                        }
+
+                        if(cmtdata.rc && cmtdata.rc.length && canAdmin){
+                            var id = "popdel"+self.linkdata.id+"thread";
+                            ul.append($("<li>").append(
+                                $("<input>", { type: "checkbox", value: "thread", id: id}),
+                                $("<label>", { for: id }).text("Delete thread (all subcomments)")
+                            ));
+                        }
+
+                        ul.append($("<li>", { class: "submit" }).append(
+                            $("<input>", { type: "button", value: "Delete"})
+                                .click(deletecomment),
+
+                            $("<input>", { type: "button", value: "Cancel" })
+                                .click(function(){self.element.ajaxtip("cancel")}),
+
+                            $("<div class='note'>shift-click to delete without options</div>")
+                        ));
+
+                        return form;
+                }
+            });
+        });
+    }
+});
+
+})(jQuery);
+
+jQuery(function($) {
+    if ( ! $.isEmptyObject( window.LJ_cmtinfo ) ) {
+        $('a')
+            .filter("[href^='"+Site.siteroot+"/talkscreen']")
+                .moderate({
+                    journal: LJ_cmtinfo.journal,
+                    form_auth: LJ_cmtinfo.form_auth
+                })
+                .end()
+            .filter("[href^='"+Site.siteroot+"/delcomment']")
+                .delcomment({
+                    journal: LJ_cmtinfo.journal,
+                    form_auth: LJ_cmtinfo.form_auth,
+                    cmtinfo: LJ_cmtinfo
+                })
+    }
+});
diff -r 1c0749606568 -r 97e54a4c0a87 htdocs/js/tooltip.dynamic.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/tooltip.dynamic.js	Mon Mar 28 16:25:31 2011 +0800
@@ -0,0 +1,150 @@
+/**
+ * @license 
+ * jQuery Tools 1.2.5 / Tooltip Dynamic Positioning
+ * 
+ * NO COPYRIGHTS OR LICENSES. DO WHAT YOU LIKE.
+ * 
+ * http://flowplayer.org/tools/tooltip/dynamic.html
+ *
+ * Since: July 2009
+ * Date:    Wed Sep 22 06:02:10 2010 +0000 
+ */
+(function($) { 
+
+	// version number
+	var t = $.tools.tooltip;
+	
+	t.dynamic = {
+		conf: {
+			classNames: "top right bottom left"
+		}
+	};
+		
+	/* 
+	 * See if element is on the viewport. Returns an boolean array specifying which
+	 * edges are hidden. Edges are in following order:
+	 * 
+	 * [top, right, bottom, left]
+	 * 
+	 * For example following return value means that top and right edges are hidden
+	 * 
+	 * [true, true, false, false]
+	 * 
+	 */
+	function getCropping(el) {
+		
+		var w = $(window); 
+		var right = w.width() + w.scrollLeft();
+		var bottom = w.height() + w.scrollTop();		
+		
+		return [
+			el.offset().top <= w.scrollTop(), 						// top
+			right <= el.offset().left + el.width(),				// right
+			bottom <= el.offset().top + el.height(),			// bottom
+			w.scrollLeft() >= el.offset().left 					// left
+		]; 
+	}
+	
+	/*
+		Returns true if all edges of an element are on viewport. false if not
+		
+		@param crop the cropping array returned by getCropping function
+	 */
+	function isVisible(crop) {
+		var i = crop.length;
+		while (i--) {
+			if (crop[i]) { return false; }	
+		}
+		return true;
+	}
+	
+	// dynamic plugin
+	$.fn.dynamic = function(conf) {
+		
+		if (typeof conf == 'number') { conf = {speed: conf}; }
+		
+		conf = $.extend({}, t.dynamic.conf, conf);
+		
+		var cls = conf.classNames.split(/\s/), orig;	
+			
+		this.each(function() {		
+				
+			var api = $(this).tooltip().onBeforeShow(function(e, pos) {				
+
+				// get nessessary variables
+				var tip = this.getTip(), tipConf = this.getConf();  
+
+				/*
+					We store the original configuration and use it to restore back to the original state.
+				*/					
+				if (!orig) {
+					orig = [
+						tipConf.position[0], 
+						tipConf.position[1], 
+						tipConf.offset[0], 
+						tipConf.offset[1], 
+						$.extend({}, tipConf)
+					];
+				}
+				
+				/*
+					display tip in it's default position and by setting visibility to hidden.
+					this way we can check whether it will be on the viewport
+				*/
+				$.extend(tipConf, orig[4]);
+				tipConf.position = [orig[0], orig[1]];
+				tipConf.offset = [orig[2], orig[3]];
+
+				tip.css({
+					visibility: 'hidden',
+					position: 'absolute',
+					top: pos.top,
+					left: pos.left 
+				}).show(); 
+				
+				// now let's see for hidden edges
+				var crop = getCropping(tip);		
+								
+				// possibly alter the configuration
+				if (!isVisible(crop)) {
+					
+					// change the position and add class
+					if (crop[2]) { $.extend(tipConf, conf.top);		tipConf.position[0] = 'top'; 		tip.addClass(cls[0]); }
+					if (crop[3]) { $.extend(tipConf, conf.right);	tipConf.position[1] = 'right'; 	tip.addClass(cls[1]); }					
+					if (crop[0]) { $.extend(tipConf, conf.bottom); 	tipConf.position[0] = 'bottom';	tip.addClass(cls[2]); } 
+					if (crop[1]) { $.extend(tipConf, conf.left);		tipConf.position[1] = 'left'; 	tip.addClass(cls[3]); }					
+					
+					// vertical offset
+					if (crop[0] || crop[2]) { tipConf.offset[0] *= -1; }
+					
+					// horizontal offset
+					if (crop[1] || crop[3]) { tipConf.offset[1] *= -1; }
+				}  
+				
+				tip.css({visibility: 'visible'}).hide();
+		
+			});
+			
+			// restore positioning as soon as possible
+			api.onBeforeShow(function() {
+				var c = this.getConf(), tip = this.getTip();		 
+				setTimeout(function() { 
+					c.position = [orig[0], orig[1]];
+					c.offset = [orig[2], orig[3]];
+				}, 0);
+			});
+			
+			// remove custom class names and restore original effect
+			api.onHide(function() {
+				var tip = this.getTip(); 
+				tip.removeClass(conf.classNames);
+			});
+				
+			ret = api;
+			
+		});
+		
+		return conf.api ? ret : this;
+	};	
+	
+}) (jQuery);
diff -r 1c0749606568 -r 97e54a4c0a87 htdocs/js/tooltip.dynamic.min.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/tooltip.dynamic.min.js	Mon Mar 28 16:25:31 2011 +0800
@@ -0,0 +1,14 @@
+/*
+ 
+ jQuery Tools 1.2.5 / Tooltip Dynamic Positioning
+
+ NO COPYRIGHTS OR LICENSES. DO WHAT YOU LIKE.
+
+ http://flowplayer.org/tools/tooltip/dynamic.html
+
+ Since: July 2009
+ Date:    Wed Sep 22 06:02:10 2010 +0000 
+*/
+(function(g){function j(a){var c=g(window),d=c.width()+c.scrollLeft(),h=c.height()+c.scrollTop();return[a.offset().top<=c.scrollTop(),d<=a.offset().left+a.width(),h<=a.offset().top+a.height(),c.scrollLeft()>=a.offset().left]}function k(a){for(var c=a.length;c--;)if(a[c])return false;return true}var i=g.tools.tooltip;i.dynamic={conf:{classNames:"top right bottom left"}};g.fn.dynamic=function(a){if(typeof a=="number")a={speed:a};a=g.extend({},i.dynamic.conf,a);var c=a.classNames.split(/\s/),d;this.each(function(){var h=
+g(this).tooltip().onBeforeShow(function(e,f){e=this.getTip();var b=this.getConf();d||(d=[b.position[0],b.position[1],b.offset[0],b.offset[1],g.extend({},b)]);g.extend(b,d[4]);b.position=[d[0],d[1]];b.offset=[d[2],d[3]];e.css({visibility:"hidden",position:"absolute",top:f.top,left:f.left}).show();f=j(e);if(!k(f)){if(f[2]){g.extend(b,a.top);b.position[0]="top";e.addClass(c[0])}if(f[3]){g.extend(b,a.right);b.position[1]="right";e.addClass(c[1])}if(f[0]){g.extend(b,a.bottom);b.position[0]="bottom";e.addClass(c[2])}if(f[1]){g.extend(b,
+a.left);b.position[1]="left";e.addClass(c[3])}if(f[0]||f[2])b.offset[0]*=-1;if(f[1]||f[3])b.offset[1]*=-1}e.css({visibility:"visible"}).hide()});h.onBeforeShow(function(){var e=this.getConf();this.getTip();setTimeout(function(){e.position=[d[0],d[1]];e.offset=[d[2],d[3]]},0)});h.onHide(function(){var e=this.getTip();e.removeClass(a.classNames)});ret=h});return a.api?ret:this}})(jQuery);
diff -r 1c0749606568 -r 97e54a4c0a87 htdocs/js/tooltip.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/tooltip.js	Mon Mar 28 16:25:31 2011 +0800
@@ -0,0 +1,340 @@
+/**
+ * @license 
+ * jQuery Tools 1.2.5 Tooltip - UI essentials
+ * 
+ * NO COPYRIGHTS OR LICENSES. DO WHAT YOU LIKE.
+ * 
+ * http://flowplayer.org/tools/tooltip/
+ *
+ * Since: November 2008
+ * Date:    Wed Sep 22 06:02:10 2010 +0000 
+ */
+(function($) { 	
+	// static constructs
+	$.tools = $.tools || {version: '1.2.5'};
+	
+	$.tools.tooltip = {
+		
+		conf: { 
+			
+			// default effect variables
+			effect: 'toggle',			
+			fadeOutSpeed: "fast",
+			predelay: 0,
+			delay: 30,
+			opacity: 1,			
+			tip: 0,
+			
+			// 'top', 'bottom', 'right', 'left', 'center'
+			position: ['top', 'center'], 
+			offset: [0, 0],
+			relative: false,
+			cancelDefault: true,
+			
+			// type to event mapping 
+			events: {
+				def: 			"mouseenter,mouseleave",
+				input: 		"focus,blur",
+				widget:		"focus mouseenter,blur mouseleave",
+				tooltip:		"mouseenter,mouseleave"
+			},
+			
+			// 1.2
+			layout: '<div/>',
+			tipClass: 'tooltip'
+		},
+		
+		addEffect: function(name, loadFn, hideFn) {
+			effects[name] = [loadFn, hideFn];	
+		} 
+	};
+	
+	
+	var effects = { 
+		toggle: [ 
+			function(done) { 
+				var conf = this.getConf(), tip = this.getTip(), o = conf.opacity;
+				if (o < 1) { tip.css({opacity: o}); }
+				tip.show();
+				done.call();
+			},
+			
+			function(done) { 
+				this.getTip().hide();
+				done.call();
+			} 
+		],
+		
+		fade: [
+			function(done) { 
+				var conf = this.getConf();
+				this.getTip().fadeTo(conf.fadeInSpeed, conf.opacity, done); 
+			},  
+			function(done) { 
+				this.getTip().fadeOut(this.getConf().fadeOutSpeed, done); 
+			} 
+		]		
+	};   
+
+		
+	/* calculate tip position relative to the trigger */  	
+	function getPosition(trigger, tip, conf) {	
+
+		
+		// get origin top/left position 
+		var top = conf.relative ? trigger.position().top : trigger.offset().top, 
+			 left = conf.relative ? trigger.position().left : trigger.offset().left,
+			 pos = conf.position[0];
+
+		top  -= tip.outerHeight() - conf.offset[0];
+		left += trigger.outerWidth() + conf.offset[1];
+		
+		// iPad position fix
+		if (/iPad/i.test(navigator.userAgent)) {
+			top -= $(window).scrollTop();
+		}
+		
+		// adjust Y		
+		var height = tip.outerHeight() + trigger.outerHeight();
+		if (pos == 'center') 	{ top += height / 2; }
+		if (pos == 'bottom') 	{ top += height; }
+		
+		
+		// adjust X
+		pos = conf.position[1]; 	
+		var width = tip.outerWidth() + trigger.outerWidth();
+		if (pos == 'center') 	{ left -= width / 2; }
+		if (pos == 'left')   	{ left -= width; }	 
+		
+		return {top: top, left: left};
+	}		
+
+	
+	
+	function Tooltip(trigger, conf) {
+		var self = this, 
+			 fire = trigger.add(self),
+			 tip,
+			 timer = 0,
+			 pretimer = 0, 
+			 title = trigger.attr("title"),
+			 tipAttr = trigger.attr("data-tooltip"),
+			 effect = effects[conf.effect],
+			 shown,
+				 
+			 // get show/hide configuration
+			 isInput = trigger.is(":input"), 
+			 isWidget = isInput && trigger.is(":checkbox, :radio, select, :button, :submit"),			
+			 type = trigger.attr("type"),
+			 evt = conf.events[type] || conf.events[isInput ? (isWidget ? 'widget' : 'input') : 'def']; 
+
+		// check that configuration is sane
+		if (!effect) { throw "Nonexistent effect \"" + conf.effect + "\""; }					
+		
+		evt = evt.split(/,\s*/); 
+		if (evt.length != 2) { throw "Tooltip: bad events configuration for " + type; } 
+		
+		
+		// trigger --> show  
+		trigger.bind(evt[0], function(e) {
+			clearTimeout(timer);
+			if (conf.predelay) {
+				pretimer = setTimeout(function() { self.show(e); }, conf.predelay);	
+				
+			} else {
+				self.show(e);	
+			}
+			
+		// trigger --> hide
+		}).bind(evt[1], function(e)  {
+			clearTimeout(pretimer);
+			if (conf.delay)  {
+				timer = setTimeout(function() { self.hide(e); }, conf.delay);	
+				
+			} else {
+				self.hide(e);		
+			}
+			
+		}); 
+		
+		
+		// remove default title
+		if (title && conf.cancelDefault) { 
+			trigger.removeAttr("title");
+			trigger.data("title", title);			
+		}		
+		
+		$.extend(self, {
+				
+			show: function(e) {  
+
+				// tip not initialized yet
+				if (!tip) {
+					
+					// data-tooltip 
+					if (tipAttr) {
+						tip = $(tipAttr);
+
+					// single tip element for all
+					} else if (conf.tip) { 
+						tip = $(conf.tip).eq(0);
+						
+					// autogenerated tooltip
+					} else if (title) { 
+						tip = $(conf.layout).addClass(conf.tipClass).appendTo(document.body)
+							.hide().append(title);
+
+					// manual tooltip
+					} else {	
+						tip = trigger.next();  
+						if (!tip.length) { tip = trigger.parent().next(); } 	 
+					}
+					
+					if (!tip.length) { throw "Cannot find tooltip for " + trigger;	}
+				} 
+			 	
+			 	if (self.isShown()) { return self; }  
+				
+			 	// stop previous animation
+			 	tip.stop(true, true); 			 	
+			 	
+				// get position
+				var pos = getPosition(trigger, tip, conf);			
+		
+				// restore title for single tooltip element
+				if (conf.tip) {
+					tip.html(trigger.data("title"));
+				}
+
+				// onBeforeShow
+				e = e || $.Event();
+				e.type = "onBeforeShow";
+				fire.trigger(e, [pos]);				
+				if (e.isDefaultPrevented()) { return self; }
+		
+				
+				// onBeforeShow may have altered the configuration
+				pos = getPosition(trigger, tip, conf);
+				
+				// set position
+				tip.css({position:'absolute', top: pos.top, left: pos.left});					
+				
+				shown = true;
+				
+				// invoke effect 
+				effect[0].call(self, function() {
+					e.type = "onShow";
+					shown = 'full';
+					fire.trigger(e);		 
+				});					
+
+	 	
+				// tooltip events       
+				var event = conf.events.tooltip.split(/,\s*/);
+
+				if (!tip.data("__set")) {
+					
+					tip.bind(event[0], function() { 
+						clearTimeout(timer);
+						clearTimeout(pretimer);
+					});
+					
+					if (event[1] && !trigger.is("input:not(:checkbox, :radio), textarea")) { 					
+						tip.bind(event[1], function(e) {
+	
+							// being moved to the trigger element
+							if (e.relatedTarget != trigger[0]) {
+								trigger.trigger(evt[1].split(" ")[0]);
+							}
+						}); 
+					} 
+					
+					tip.data("__set", true);
+				}
+				
+				return self;
+			},
+			
+			hide: function(e) {
+
+				if (!tip || !self.isShown()) { return self; }
+			
+				// onBeforeHide
+				e = e || $.Event();
+				e.type = "onBeforeHide";
+				fire.trigger(e);				
+				if (e.isDefaultPrevented()) { return; }
+	
+				shown = false;
+				
+				effects[conf.effect][1].call(self, function() {
+					e.type = "onHide";
+					fire.trigger(e);		 
+				});
+				
+				return self;
+			},
+			
+			isShown: function(fully) {
+				return fully ? shown == 'full' : shown;	
+			},
+				
+			getConf: function() {
+				return conf;	
+			},
+				
+			getTip: function() {
+				return tip;	
+			},
+			
+			getTrigger: function() {
+				return trigger;	
+			}		
+
+		});		
+
+		// callbacks	
+		$.each("onHide,onBeforeShow,onShow,onBeforeHide".split(","), function(i, name) {
+				
+			// configuration
+			if ($.isFunction(conf[name])) { 
+				$(self).bind(name, conf[name]); 
+			}
+
+			// API
+			self[name] = function(fn) {
+				if (fn) { $(self).bind(name, fn); }
+				return self;
+			};
+		});
+		
+	}
+		
+	
+	// jQuery plugin implementation
+	$.fn.tooltip = function(conf) {
+		
+		// return existing instance
+		var api = this.data("tooltip");
+		if (api) { return api; }
+
+		conf = $.extend(true, {}, $.tools.tooltip.conf, conf);
+		
+		// position can also be given as string
+		if (typeof conf.position == 'string') {
+			conf.position = conf.position.split(/,?\s/);	
+		}
+		
+		// install tooltip for each entry in jQuery object
+		this.each(function() {
+			api = new Tooltip($(this), conf); 
+			$(this).data("tooltip", api); 
+		});
+		
+		return conf.api ? api: this;		 
+	};
+		
+}) (jQuery);
+
+		
+
diff -r 1c0749606568 -r 97e54a4c0a87 htdocs/js/tooltip.min.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/tooltip.min.js	Mon Mar 28 16:25:31 2011 +0800
@@ -0,0 +1,18 @@
+/*
+ 
+ jQuery Tools 1.2.5 Tooltip - UI essentials
+
+ NO COPYRIGHTS OR LICENSES. DO WHAT YOU LIKE.
+
+ http://flowplayer.org/tools/tooltip/
+
+ Since: November 2008
+ Date:    Wed Sep 22 06:02:10 2010 +0000 
+*/
+(function(f){function p(a,b,c){var h=c.relative?a.position().top:a.offset().top,d=c.relative?a.position().left:a.offset().left,i=c.position[0];h-=b.outerHeight()-c.offset[0];d+=a.outerWidth()+c.offset[1];if(/iPad/i.test(navigator.userAgent))h-=f(window).scrollTop();var j=b.outerHeight()+a.outerHeight();if(i=="center")h+=j/2;if(i=="bottom")h+=j;i=c.position[1];a=b.outerWidth()+a.outerWidth();if(i=="center")d-=a/2;if(i=="left")d-=a;return{top:h,left:d}}function u(a,b){var c=this,h=a.add(c),d,i=0,j=
+0,m=a.attr("title"),q=a.attr("data-tooltip"),r=o[b.effect],l,s=a.is(":input"),v=s&&a.is(":checkbox, :radio, select, :button, :submit"),t=a.attr("type"),k=b.events[t]||b.events[s?v?"widget":"input":"def"];if(!r)throw'Nonexistent effect "'+b.effect+'"';k=k.split(/,\s*/);if(k.length!=2)throw"Tooltip: bad events configuration for "+t;a.bind(k[0],function(e){clearTimeout(i);if(b.predelay)j=setTimeout(function(){c.show(e)},b.predelay);else c.show(e)}).bind(k[1],function(e){clearTimeout(j);if(b.delay)i=
+setTimeout(function(){c.hide(e)},b.delay);else c.hide(e)});if(m&&b.cancelDefault){a.removeAttr("title");a.data("title",m)}f.extend(c,{show:function(e){if(!d){if(q)d=f(q);else if(b.tip)d=f(b.tip).eq(0);else if(m)d=f(b.layout).addClass(b.tipClass).appendTo(document.body).hide().append(m);else{d=a.next();d.length||(d=a.parent().next())}if(!d.length)throw"Cannot find tooltip for "+a;}if(c.isShown())return c;d.stop(true,true);var g=p(a,d,b);b.tip&&d.html(a.data("title"));e=e||f.Event();e.type="onBeforeShow";
+h.trigger(e,[g]);if(e.isDefaultPrevented())return c;g=p(a,d,b);d.css({position:"absolute",top:g.top,left:g.left});l=true;r[0].call(c,function(){e.type="onShow";l="full";h.trigger(e)});g=b.events.tooltip.split(/,\s*/);if(!d.data("__set")){d.bind(g[0],function(){clearTimeout(i);clearTimeout(j)});g[1]&&!a.is("input:not(:checkbox, :radio), textarea")&&d.bind(g[1],function(n){n.relatedTarget!=a[0]&&a.trigger(k[1].split(" ")[0])});d.data("__set",true)}return c},hide:function(e){if(!d||!c.isShown())return c;
+e=e||f.Event();e.type="onBeforeHide";h.trigger(e);if(!e.isDefaultPrevented()){l=false;o[b.effect][1].call(c,function(){e.type="onHide";h.trigger(e)});return c}},isShown:function(e){return e?l=="full":l},getConf:function(){return b},getTip:function(){return d},getTrigger:function(){return a}});f.each("onHide,onBeforeShow,onShow,onBeforeHide".split(","),function(e,g){f.isFunction(b[g])&&f(c).bind(g,b[g]);c[g]=function(n){n&&f(c).bind(g,n);return c}})}f.tools=f.tools||{version:"1.2.5"};f.tools.tooltip=
+{conf:{effect:"toggle",fadeOutSpeed:"fast",predelay:0,delay:30,opacity:1,tip:0,position:["top","center"],offset:[0,0],relative:false,cancelDefault:true,events:{def:"mouseenter,mouseleave",input:"focus,blur",widget:"focus mouseenter,blur mouseleave",tooltip:"mouseenter,mouseleave"},layout:"<div/>",tipClass:"tooltip"},addEffect:function(a,b,c){o[a]=[b,c]}};var o={toggle:[function(a){var b=this.getConf(),c=this.getTip();b=b.opacity;b<1&&c.css({opacity:b});c.show();a.call()},function(a){this.getTip().hide();
+a.call()}],fade:[function(a){var b=this.getConf();this.getTip().fadeTo(b.fadeInSpeed,b.opacity,a)},function(a){this.getTip().fadeOut(this.getConf().fadeOutSpeed,a)}]};f.fn.tooltip=function(a){var b=this.data("tooltip");if(b)return b;a=f.extend(true,{},f.tools.tooltip.conf,a);if(typeof a.position=="string")a.position=a.position.split(/,?\s/);this.each(function(){b=new u(f(this),a);f(this).data("tooltip",b)});return a.api?b:this}})(jQuery);
diff -r 1c0749606568 -r 97e54a4c0a87 htdocs/stc/ajaxtip.css
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/stc/ajaxtip.css	Mon Mar 28 16:25:31 2011 +0800
@@ -0,0 +1,13 @@
+.ajaxtooltip {
+    padding: .2em .5em;
+    z-index: 99;
+    text-align: left;
+    color: #000;
+    background: #fff;
+    border: 2px solid #555;
+}
+.ajaxresult {
+    color: #000;
+    background: #fbf9ee;
+    border: 2px solid #fcefa1;
+}
diff -r 1c0749606568 -r 97e54a4c0a87 htdocs/stc/popup-form.css
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/stc/popup-form.css	Mon Mar 28 16:25:31 2011 +0800
@@ -0,0 +1,49 @@
+.popup-form fieldset {
+    border-style: none;
+}
+.popup-form legend {
+    font-weight: bold;
+}
+.popup-form ul {
+    margin: 0;
+    padding: 0;
+}
+.popup-form ul li {
+    background: none !important;
+    display: block !important;
+}
+.popup-form .submit {
+    margin-top: 0.7em;
+}
+.popup-form .note {
+    font-size: 0.8em;
+    font-style: italic;
+    line-height: 1.3em;
+}
+.submit input {
+    margin-right: 1em;
+    background: #e3e3e3;
+    border: 1px solid #ccc;
+    -moz-border-radius: 3px;
+    border-radius: 3px;
+    -moz-box-shadow: inset 0 0 1px 1px #f6f6f6;
+    box-shadow: inset 0 0 1px 1px #f6f6f6;
+    color: #333;
+    font-weight: bold;
+    padding: 8px 3em 9px;
+    text-shadow: 0 1px 0px #fff;
+}
+
+.submit input:hover {
+    background: #c9c9c9;
+    -moz-box-shadow: inset 0 0 1px 1px #eaeaea;
+    box-shadow: inset 0 0 1px 1px #eaeaea;
+    color: #222;
+}
+
+.submit input:active {
+    background: #d0d0d0;
+    -moz-box-shadow: inset 0 0 1px 1px #e3e3e3;
+    box-shadow: inset 0 0 1px 1px #e3e3e3;
+    color: #000;
+}
diff -r 1c0749606568 -r 97e54a4c0a87 htdocs/talkscreen.bml
--- a/htdocs/talkscreen.bml	Mon Mar 28 14:40:04 2011 +0800
+++ b/htdocs/talkscreen.bml	Mon Mar 28 16:25:31 2011 +0800
@@ -30,7 +30,12 @@ _info?><?_code
     my $error = sub {
         if ($jsmode) {
             BML::finish();
-            return "alert('" . LJ::ejs($_[0]) . "'); 0;";
+            # FIXME: remove once we've switched over completely to jquery
+            if ( !!$GET{json} ) {
+                return JSON::objToJson( { error => $_[0] } );
+            } else {
+                return "alert('" . LJ::ejs($_[0]) . "'); 0;";
+            }
         }
         $body = "<?h1 $ML{'Error'} h1?><?p $_[0] p?>";
         return;
@@ -48,7 +53,9 @@ _info?><?_code
     my $dtalkid = $qtalkid;   # display talkid, for use in URL later
 
     my $jsres = sub {
-        my $mode = shift;
+        use JSON;
+
+        my ( $mode, $message ) = @_;
 
         # flip case of 'un'
         my $newmode = "un$mode";
@@ -63,14 +70,20 @@ _info?><?_code
             'unfreeze' => "silk/comments/unfreeze.png",
         };
 
-        my $res = "rpcRes = {\n mode: \"$mode\", \n" .
-            " newalt: \"$alttext\", id: $dtalkid, \n" .
-            " oldimage: \"$LJ::IMGPREFIX/$stockimg->{$mode}\",\n " .
-            " newimage: '$LJ::IMGPREFIX/$stockimg->{$newmode}',\n " .
-            " newurl: '$LJ::SITEROOT/talkscreen?mode=$newmode&journal=$journal&talkid=$dtalkid' \n" .
-            "};\n";
+        my %ret = (
+            id       => $dtalkid,
+            mode     => $mode,
+            newalt   => $alttext,
+            oldimage => "$LJ::IMGPREFIX/$stockimg->{$mode}",
+            newimage => "$LJ::IMGPREFIX/$stockimg->{$newmode}",
+            newurl   => "$LJ::SITEROOT/talkscreen?mode=$newmode&journal=$journal&talkid=$dtalkid",
+            msg      => $message,
+        );
+
+        sleep 1 if $LJ::IS_DEV_SERVER;
+
         BML::finish();
-        return $res;
+        return JSON::objToJson( \%ret );
     };
 
     my $remote = LJ::get_remote();
@@ -81,7 +94,7 @@ _info?><?_code
     # userpost (username of this comment's author). Then we can check permissions.
 
     my $u = LJ::load_user($journal);
-    return $error->($ML{'.talk.error.bogusargs'}) unless $u;
+    return $error->($ML{'talk.error.bogusargs'}) unless $u;
 
     # if we're on a user vhost, our remote was authed using that vhost,
     # so let's let them only modify the journal that their session
@@ -148,7 +161,7 @@ _info?><?_code
             LJ::Talk::screen_comment($u, $qitemid, $qtalkid);
         }
         # FIXME: no error checking?
-        return $jsres->($mode) if $jsmode;
+        return $jsres->($mode, $ML{'.screened.body'}) if $jsmode;
         $body = "<?h1 $ML{'.screened.title'} h1?><?p $ML{'.screened.body'} $linktext p?>";
         return;
     }
@@ -173,7 +186,7 @@ _info?><?_code
             LJ::Talk::unscreen_comment($u, $qitemid, $qtalkid);
         }
         # FIXME: no error checking?
-        return $jsres->($mode) if $jsmode;
+        return $jsres->($mode, $ML{'.unscreened.body'}) if $jsmode;
         $body = "<?h1 $ML{'.unscreened.title'} h1?><?p $ML{'.unscreened.body'} $linktext p?>";
         return;
     }
@@ -201,7 +214,7 @@ _info?><?_code
         if ($state ne 'F') {
             LJ::Talk::freeze_thread($u, $qitemid, $qtalkid);
         }
-        return $jsres->($mode) if $jsmode;
+        return $jsres->($mode, $ML{'.frozen.body'}) if $jsmode;
         $body = "<?h1 $ML{'.frozen.title'} h1?><?p $ML{'.frozen.body'} $linktext p?>";
         return;
     }
@@ -227,7 +240,7 @@ _info?><?_code
         if ($state eq 'F') {
             LJ::Talk::unfreeze_thread($u, $qitemid, $qtalkid);
         }
-        return $jsres->($mode) if $jsmode;
+        return $jsres->($mode, $ML{'.unfrozen.body'}) if $jsmode;
         $body = "<?h1 $ML{'.unfrozen.title'} h1?><?p $ML{'.unfrozen.body'} $linktext p?>";
         return;
     }
diff -r 1c0749606568 -r 97e54a4c0a87 views/dev/tests/commentmanage.html
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/views/dev/tests/commentmanage.html	Mon Mar 28 16:25:31 2011 +0800
@@ -0,0 +1,57 @@
+<a id="url_with_params_noescape" href="http://blah.com/?noequals&novalue=&key=value&key2=value%202">not escaped</a>
+<a id="url_with_params_escaped" href="http://blah.com/?noequals&amp;novalue=&amp;key=value&amp;key2=value%202">not escaped</a>
+
+
+<!-- old JS implementation really really needs the delete link to be the first in this-->
+<!-- text -->
+<div id="cmt123">comment 123
+
+
+<a id="delete_link" href="http://localhost/delcomment?journal=test&amp;id=123">Delete</a>
+
+<a id="freeze_link" href="http://localhost/talkscreen?mode=freeze&amp;journal=test&amp;talkid=123">Freeze</a>
+<a id="unfreeze_link" href="http://localhost/talkscreen?mode=unfreeze&amp;journal=test&amp;talkid=123">Unfreeze</a>
+
+<a id="screen_link" href="http://localhost/talkscreen?mode=screen&amp;journal=test&amp;talkid=123">Screen</a>
+<a id="unscreen_link" href="http://localhost/talkscreen?mode=unscreen&amp;journal=test&amp;talkid=123">Unscreen</a>
+
+
+<!-- images -->
+<a id="delete_img" href="http://localhost/delcomment?journal=test&amp;id=123"><img border="0" width="16" height="16" alt="Delete" title="Delete" src="http://localhost/img/silk/comments/delete.png" /></a>
+
+<a id="freeze_img" href="http://localhost/talkscreen?mode=freeze&amp;journal=test&amp;talkid=123"><img border="0" width="16" height="16" alt="Freeze" title="Freeze" src="http://localhost/img/silk/comments/freeze.png" /></a>
+<a id="unfreeze_img" href="http://localhost/talkscreen?mode=unfreeze&amp;journal=test&amp;talkid=123"><img border="0" width="16" height="16" alt="Unfreeze" title="Unfreeze" src="http://localhost/img/silk/comments/unfreeze.png" /></a>
+
+<a id="screen_img" href="http://localhost/talkscreen?mode=screen&amp;journal=test&amp;talkid=123"><img border="0" width="16" height="16" alt="Screen" title="Screen" src="http://localhost/img/silk/comments/screen.png" /></a>
+<a id="unscreen_img" href="http://localhost/talkscreen?mode=unscreen&amp;journal=test&amp;talkid=123"><img border="0" width="16" height="16" alt="Unscreen" title="Unscreen" src="http://localhost/img/silk/comments/unscreen.png" /></a>
+</div>
+
+<div id="cmt456">comment 456; child comment
+<a id="child_delete_link" href="http://localhost/delcomment?journal=test&amp;id=456">Delete</a>
+
+<a id="child_freeze_link" href="http://localhost/talkscreen?mode=freeze&amp;journal=test&amp;talkid=456">Freeze</a>
+<a id="child_unfreeze_link" href="http://localhost/talkscreen?mode=unfreeze&amp;journal=test&amp;talkid=456">Unfreeze</a>
+
+<a id="child_screen_link" href="http://localhost/talkscreen?mode=screen&amp;journal=test&amp;talkid=456">Screen</a>
+<a id="child_unscreen_link" href="http://localhost/talkscreen?mode=unscreen&amp;journal=test&amp;talkid=456">Unscreen</a>
+
+<!-- images -->
+<a id="child_delete_img" href="http://localhost/delcomment?journal=test&amp;id=456"><img border="0" width="16" height="16" alt="Delete" title="Delete" src="http://localhost/img/silk/comments/delete.png" /></a>
+
+<a id="child_freeze_img" href="http://localhost/talkscreen?mode=freeze&amp;journal=test&amp;talkid=456"><img border="0" width="16" height="16" alt="Freeze" title="Freeze" src="http://localhost/img/silk/comments/freeze.png" /></a>
+<a id="child_unfreeze_img" href="http://localhost/talkscreen?mode=unfreeze&amp;journal=test&amp;talkid=456"><img border="0" width="16" height="16" alt="Unfreeze" title="Unfreeze" src="http://localhost/img/silk/comments/unfreeze.png" /></a>
+
+<a id="child_screen_img" href="http://localhost/talkscreen?mode=screen&amp;journal=test&amp;talkid=456"><img border="0" width="16" height="16" alt="Screen" title="Screen" src="http://localhost/img/silk/comments/screen.png" /></a>
+<a id="child_unscreen_img" href="http://localhost/talkscreen?mode=unscreen&amp;journal=test&amp;talkid=456"><img border="0" width="16" height="16" alt="Unscreen" title="Unscreen" src="http://localhost/img/silk/comments/unscreen.png" /></a>
+</div>
+
+<div id="invalidlink">invalid links here
+<a id="invalid_delete_link" href="http://localhost/delcomment">Delete</a>
+<a id="invalid_moderate_link" href="http://localhost/talkscreen">Moderate</a>
+</div>
+
+<a id="mismatched_delete_link" href="http://localhost/delcomment?journal=test&amp;id=999">Delete</a>
+<a id="mismatched_moderate_link" href="http://localhost/talkscreen?mode=freeze&amp;journal=test&amp;talkid=999">Freeze</a>
+
+<a id="mismatched_journal_delete_link" href="http://localhost/delcomment?journal=untest&amp;id=123">Delete</a>
+<a id="mismatched_journal_moderate_link" href="http://localhost/talkscreen?mode=freeze&amp;journal=untest&amp;talkid=123">Freeze</a>
diff -r 1c0749606568 -r 97e54a4c0a87 views/dev/tests/commentmanage.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/views/dev/tests/commentmanage.js	Mon Mar 28 16:25:31 2011 +0800
@@ -0,0 +1,545 @@
+/* INCLUDE:
+
+old: js/commentmanage.js
+jquery: js/jquery/jquery.ui.widget.js
+jquery: js/jquery.ajaxtip.js
+jquery: js/jquery.commentmanage.js
+jquery: js/tooltip.js
+jquery: js/tooltip.dynamic.js
+*/
+
+var lifecycle = {
+    setup: function() {
+        var p = {
+            "freeze": {
+                "mode": "freeze",
+                "text": "Freeze",
+                "img": "http://localhost/img/silk/comments/freeze.png",
+                "url": "http://localhost/talkscreen?mode=freeze&journal=test&talkid=123",
+                "msg": "thread was frozen"
+            },
+
+            "unfreeze": {
+                "mode": "unfreeze",
+                "text": "Unfreeze",
+                "img": "http://localhost/img/silk/comments/unfreeze.png",
+                "url": "http://localhost/talkscreen?mode=unfreeze&journal=test&talkid=123",
+                "msg": "thread was unfrozen"
+            },
+
+            "screen": {
+                "mode": "screen",
+                "text": "Screen",
+                "img": "http://localhost/img/silk/comments/screen.png",
+                "url": "http://localhost/talkscreen?mode=screen&journal=test&talkid=123",
+                "msg": "comment was screened"
+            },
+
+            "unscreen": {
+                "mode": "unscreen",
+                "text": "Unscreen",
+                "img": "http://localhost/img/silk/comments/unscreen.png",
+                "url": "http://localhost/talkscreen?mode=unscreen&journal=test&talkid=123",
+                "msg": "comment was unscreened"
+            },
+
+            "delete": {
+                "mode": "delete",
+                "text": "Delete",
+                "img": "http://localhost/img/silk/comments/delete.png",
+                "url": "http://localhost/delcomment?journal=test&id=123",
+                "msg": "comment deleted"
+            }
+        };
+        this.linkprops = p;
+
+        this.del_args = {
+                cmtinfo: {
+                "form_auth": "authauthauth",
+                "remote": "test",
+                "journal": "test",
+
+                "canSpam": 1,
+                "canAdmin": 1,
+
+                "123": { "parent": "",
+                    "u": "test",
+                    "rc": [ "456" ],
+                    "full": 1
+                },
+                "456": {
+                    "parent": "123",
+                    "u": "test",
+                    "rc": [],
+                    "full": 1
+                }
+            },
+            journal: "test",
+            form_auth: "authauthauth"
+        };
+
+        this.mod_args = {
+            journal: "test",
+            form_auth: "authauthauth"
+        };
+
+        this.server = sinon.sandbox.useFakeServer();
+        this.server.respondWith( /mode=freeze/, [
+            200,
+            {},
+            '{\
+                "mode": "freeze",\
+                "id": 123,\
+                "newalt": "'+p.unfreeze.text+'",\
+                "oldimage": "'+p.freeze.img+'",\
+                "newimage": "'+p.unfreeze.img+'",\
+                "newurl": "'+p.unfreeze.url+'",\
+                "msg": "'+p.unfreeze.msg+'"\
+            }'
+        ] );
+
+        this.server.respondWith( /mode=unfreeze/, [
+            200,
+            {},
+            '{\
+                "mode": "unfreeze",\
+                "id": 123,\
+                "newalt": "'+p.freeze.text+'",\
+                "oldimage": "'+p.unfreeze.img+'",\
+                "newimage": "'+p.freeze.img+'",\
+                "newurl": "'+p.freeze.url+'",\
+                "msg": "'+p.freeze.msg+'"\
+            }'
+        ] );
+
+        this.server.respondWith( /mode=screen/, [
+            200,
+            {},
+            '{\
+                "mode": "screen",\
+                "id": 123,\
+                "newalt": "'+p.unscreen.text+'",\
+                "oldimage": "'+p.screen.img+'",\
+                "newimage": "'+p.unscreen.img+'",\
+                "newurl": "'+p.unscreen.url+'",\
+                "msg": "'+p.unscreen.msg+'"\
+            }'
+        ] );
+
+        this.server.respondWith( /mode=unscreen/, [
+            200,
+            {},
+            '{\
+                "mode": "unscreen",\
+                "id": 123,\
+                "newalt": "'+p.screen.text+'",\
+                "oldimage": "'+p.unscreen.img+'",\
+                "newimage": "'+p.screen.img+'",\
+                "newurl": "'+p.screen.url+'",\
+                "msg": "'+p.screen.msg+'"\
+            }'
+        ] );
+
+        this.server.respondWith( /delforcefail/ [
+            200,
+            {},
+            '{ "error": "fail!" }'
+        ] );
+
+        this.server.respondWith( /delcomment/, [
+            200,
+            {},
+            '{ "msg": "'+p["delete"].msg+'" }'
+        ] );
+
+        this.server.respondWith( [
+            200,
+            {},
+            '{ "error": "error!" }'
+        ] );
+    },
+
+    teardown: function() {
+        this.server.restore();
+    }
+};
+
+
+module( "jquery", lifecycle );
+function _check_link(linkid, oldstate, newstate) {
+    var $link = $("#"+linkid);
+
+    equal($link.attr("href"), oldstate.url, linkid + " - original url" );
+    equal($link.text(), oldstate.text, linkid + " - original text" );
+
+    $link
+        .moderate(this.mod_args)
+        .one( "moderatecomplete", function( event, data ) {
+            equal($link.attr("href"), newstate.url, linkid + " - new url" );
+            equal($link.text(), newstate.text, linkid + " - new text" );
+
+            equals($link.ajaxtip("widget").html(), newstate.msg, linkid + " - did action");
+        })
+        .trigger("click");
+    this.server.respond();
+
+    $link
+        .one("moderatecomplete", function( event, data ) {
+            equal($link.attr("href"), oldstate.url, linkid + " - changed back to old url");
+            equal($link.text(), oldstate.text, linkid + " - changed back to old text");
+
+            equals($link.ajaxtip("widget").html(), oldstate.msg, linkid + " - did action");
+        })
+        .trigger("click");
+    this.server.respond();
+}
+
+function _check_link_with_image(linkid, oldstate, newstate) {
+    var $link = $("#"+linkid);
+    var $img = $link.find("img");
+
+    equal($link.attr("href"), oldstate.url, linkid + " - original url" );
+    equal($img.attr("alt"), oldstate.text, linkid + " - original alt" );
+    equal($img.attr("title"), oldstate.text, linkid + " - original title" );
+
+    $link
+        .moderate(this.mod_args)
+        .one( "moderatecomplete", function(event, data) {
+            equal($link.attr("href"), newstate.url, linkid + " - new url" );
+            equal($img.attr("alt"), newstate.text, linkid + " - new alt" );
+            equal($img.attr("title"), newstate.text, linkid + " - new title" );
+
+            equals($link.ajaxtip("widget").html(), newstate.msg, linkid + " - did action");
+        })
+        .trigger("click");
+    this.server.respond();
+
+    $link
+        .one("moderatecomplete", function(event, data) {
+            equal($link.attr("href"), oldstate.url, linkid + " - changed back to old url");
+            equal($img.attr("alt"), oldstate.text, linkid + " - changed back to old alt");
+            equal($img.attr("title"), oldstate.text, linkid + " - changed back to old title" );
+
+            equals($link.ajaxtip("widget").html(), oldstate.msg, linkid + " - did action");
+        })
+        .trigger("click");
+        this.server.respond();
+}
+
+test( "freeze / unfreeze", 38, function() {
+    _check_link.call(this, "freeze_link", this.linkprops.freeze, this.linkprops.unfreeze);
+    _check_link.call(this, "unfreeze_link", this.linkprops.unfreeze, this.linkprops.freeze);
+
+    _check_link_with_image.call(this, "freeze_img", this.linkprops.freeze, this.linkprops.unfreeze);
+    _check_link_with_image.call(this, "unfreeze_img", this.linkprops.unfreeze, this.linkprops.freeze);
+} );
+
+test( "screen / unscreen", 38, function() {
+    _check_link.call(this, "screen_link", this.linkprops.screen, this.linkprops.unscreen);
+    _check_link.call(this, "unscreen_link", this.linkprops.unscreen, this.linkprops.screen);
+
+    _check_link_with_image.call(this, "screen_img", this.linkprops.screen, this.linkprops.unscreen);
+    _check_link_with_image.call(this, "unscreen_img", this.linkprops.unscreen, this.linkprops.screen);
+} );
+
+test( "delete with shift", 4, function() {
+    var parent = $("#cmt123");
+    var child = $("#cmt456");
+    ok( parent.is(":visible"), "Parent comment started out visible" );
+    ok( child.is(":visible"), "Child comment started out visible" );
+
+    $("#delete_link")
+        .delcomment(this.del_args)
+        .one( "delcommentcomplete", function(event, data) {
+            // finish animation early
+            parent.stop(true, true);
+            child.stop(true, true);
+
+            ok( ! parent.is(":visible"), "Parent comment successfully hidden after delete" );
+            ok(   child.is(":visible"), "Child comment not deleted, still visible" );
+
+        })
+        .trigger({type: "click", shiftKey: true});
+    this.server.respond();
+} );
+
+test( "delete all children (has children)", 4, function() {
+    var parent = $("#cmt123");
+    var child = $("#cmt456");
+    ok( parent.is(":visible"), "Parent comment started out visible" );
+    ok( child.is(":visible"), "Child comment started out visible" );
+
+    $("#delete_link")
+        .delcomment(this.del_args)
+        .one( "delcommentcomplete", function(event, data) {
+            // finish animation early
+            parent.stop(true, true);
+            child.stop(true, true);
+
+            ok( ! parent.is(":visible"), "Parent comment successfully hidden after delete" );
+            ok( ! child.is(":visible"), "Child comment successfully hidden after delete" );
+        })
+        .trigger("click")
+        .ajaxtip("widget")
+            .find("input[value='thread']")
+                .attr("checked", "checked")
+                .end()
+            .find("input[value='Delete']")
+                .click();
+
+    this.server.respond();
+} );
+
+test( "delete all children (has no children)", 4, function() {
+    var parent = $("#cmt123");
+    var child = $("#cmt456");
+    ok( parent.is(":visible"), "Parent comment started out visible" );
+    ok( child.is(":visible"), "Child comment started out visible" );
+
+    $("#child_delete_link")
+        .delcomment(this.del_args)
+        .one( "delcommentcomplete", function(event, data) {
+            // finish animation early
+            parent.stop(true, true);
+            child.stop(true, true);
+
+            ok(   parent.is(":visible"), "Parent comment not deleted" );
+            ok( ! child.is(":visible"), "Child comment successfully hidden after delete" );
+        })
+        .trigger("click")
+        .ajaxtip("widget")
+            .find("input[value='thread']")
+                .attr("checked", "checked")
+                .end()
+            .find("input[value='Delete']")
+                .click();
+    this.server.respond();
+} );
+
+test( "delete no children (has children)", 4, function() {
+    var parent = $("#cmt123");
+    var child = $("#cmt456");
+    ok( parent.is(":visible"), "Parent comment started out visible" );
+    ok( child.is(":visible"), "Child comment started out visible" );
+
+    $("#delete_link")
+        .delcomment(this.del_args)
+        .one( "delcommentcomplete", function(event, data) {
+            // finish animation early
+            parent.stop(true, true);
+            child.stop(true, true);
+
+            ok( ! parent.is(":visible"), "Parent comment successfully hidden after delete" );
+            ok(   child.is(":visible"), "Child comment not deleted, still visible" );
+        })
+        .trigger("click")
+        .ajaxtip("widget")
+            .find("input[value='Delete']")
+                .click();
+
+    this.server.respond();
+} );
+
+test( "delete no children (has no children)", 4, function() {
+    var parent = $("#cmt123");
+    var child = $("#cmt456");
+    ok( parent.is(":visible"), "Parent comment started out visible" );
+    ok( child.is(":visible"), "Child comment started out visible" );
+
+    $("#child_delete_link")
+        .delcomment(this.del_args)
+        .one( "delcommentcomplete", function(event, data) {
+            // finish animation early
+            parent.stop(true, true);
+            child.stop(true, true);
+
+            ok(   parent.is(":visible"), "Parent comment not deleted, still visible" );
+            ok( ! child.is(":visible"), "Child comment successfully hidden after delete" );
+        })
+        .trigger("click")
+        .ajaxtip("widget")
+            .find("input[value='Delete']")
+                .click();
+
+    this.server.respond();
+} );
+
+test( "failed delete: no hiding", 4, function() {
+    var parent = $("#cmt123");
+    var child = $("#cmt456");
+    ok( parent.is(":visible"), "Parent comment started out visible" );
+    ok( child.is(":visible"), "Child comment started out visible" );
+
+    this.del_args["endpoint"] = "/delforcefail";
+
+    $("#delete_link")
+        .delcomment(this.del_args)
+        .one( "delcommentcomplete", function(event, data) {
+            // finish animation early
+            parent.stop(true, true);
+            child.stop(true, true);
+
+            ok( parent.is(":visible"), "Parent comment not deleted, still visible" );
+            ok( child.is(":visible"), "Child comment not deleted, still visible" );
+        })
+        .trigger("click")
+        .ajaxtip("widget")
+            .find("input[value='Delete']")
+                .click();
+
+    this.server.respond();
+
+} );
+
+
+test( "invalid moderate link", 1, function() {
+    $("#invalid_moderate_link")
+        .moderate(this.mod_args)
+        .trigger("click")
+    this.server.respond()
+
+    equals($("#invalid_moderate_link").ajaxtip("widget").text(),
+            "Error moderating comment #. Not enough context available.");
+});
+
+test( "invalid delete link", 1, function() {
+    $("#invalid_delete_link")
+        .delcomment(this.del_args)
+        .trigger({ type: "click", shiftKey: true })
+    this.server.respond();
+
+    equals($("#invalid_delete_link").ajaxtip("widget").text(),
+            "Error deleting comment #. Comment is not visible on this page.");
+} );
+
+
+test( "no such comment for moderation", 1, function() {
+    $("#mismatched_moderate_link")
+        .moderate(this.mod_args)
+        .trigger("click")
+    this.server.respond();
+
+    equals($("#mismatched_moderate_link").ajaxtip("widget").text(),
+            "Error moderating comment #999. Cannot moderate comment which is not visible on this page.")
+} );
+
+test( "mismatched journal for deletion", 1, function() {
+    $("#mismatched_journal_delete_link")
+        .delcomment(this.del_args)
+        .trigger({ type: "click", shiftKey: true })
+    this.server.respond();
+
+    equals($("#mismatched_journal_delete_link").ajaxtip("widget").text(),
+            "Error deleting comment #123. Journal in link does not match expected journal.")
+} );
+
+test( "no such comment for moderation", 1, function() {
+    $("#mismatched_journal_moderate_link")
+        .moderate(this.mod_args)
+        .trigger("click")
+    this.server.respond();
+
+    equals($("#mismatched_journal_moderate_link").ajaxtip("widget").text(),
+            "Error moderating comment #123. Journal in link does not match expected journal.")
+} );
+
+test( "no such comment for deletion", 1, function() {
+    $("#mismatched_delete_link")
+        .delcomment(this.del_args)
+        .trigger({ type: "click", shiftKey: true })
+    this.server.respond();
+
+    equals($("#mismatched_delete_link").ajaxtip("widget").text(),
+            "Error deleting comment #999. Comment is not visible on this page.")
+} );
+
+test( "lacking arguments for moderate: form_auth", 1, function() {
+    delete this.mod_args["form_auth"]
+    $("#freeze_link")
+        .moderate(this.mod_args)
+        .trigger("click");
+    this.server.respond();
+
+    equals($("#freeze_link").ajaxtip("widget").text(),
+            "Error moderating comment #123. Not enough context available.")
+} );
+
+test( "lacking arguments for moderate: journal", 1, function() {
+    delete this.mod_args["journal"]
+    $("#freeze_link")
+        .moderate(this.mod_args)
+        .trigger("click");
+    this.server.respond();
+
+    equals($("#freeze_link").ajaxtip("widget").text(),
+            "Error moderating comment #123. Not enough context available.")
+} );
+
+test( "lacking arguments for delete: cmtinfo", 1, function() {
+    delete this.del_args["cmtinfo"]
+    $("#delete_link")
+        .delcomment(this.del_args)
+        .trigger({ type: "click", shiftKey: true })
+    this.server.respond();
+
+    equals($("#delete_link").ajaxtip("widget").text(),
+            "Error deleting comment #123. Not enough context available.")
+} );
+
+test( "lacking arguments for delete: journal", 1, function() {
+    delete this.del_args["journal"];
+    $("#delete_link")
+        .delcomment(this.del_args)
+        .trigger({ type: "click", shiftKey: true })
+    this.server.respond();
+
+    equals($("#delete_link").ajaxtip("widget").text(),
+            "Error deleting comment #123. Not enough context available.")
+} );
+
+test( "lacking arguments for delete: form_auth", 1, function() {
+    delete this.del_args["form_auth"]
+    $("#delete_link")
+        .delcomment(this.del_args)
+        .trigger({ type: "click", shiftKey: true })
+    this.server.respond();
+
+    equals($("#delete_link").ajaxtip("widget").text(),
+            "Error deleting comment #123. Not enough context available.")
+} );
+
+
+module( "jquery util" );
+test( "extract params", 18, function() {
+    var params;
+
+    params = $.extractParams("http://blah.com/");
+    deepEqual( params, {}, "no params" );
+
+    params = $.extractParams("http://blah.com/?");
+    deepEqual( params, {}, "has ?, but no params" );
+
+    params = $.extractParams("http://blah.com/?noequals&novalue=&key=value&key2=value 2");
+    equal( params["key"], "value", "extract url params: key" );
+    equal( params["noequals"], undefined, "extract url params: noequals" );
+    equal( params["novalue"], "", "extract url params: novalue" );
+    equal( params["key2"], "value 2", "extract url params: key2" );
+
+    params = $.extractParams("http://blah.com/?noequals&novalue=&key=value&key2=value%202");
+    equal( params["key"], "value", "extract url params URI-escaped: key" );
+    equal( params["noequals"], undefined, "extract url params URI-escaped: noequals" );
+    equal( params["novalue"], "", "extract url params URI-escaped: novalue" );
+    equal( params["key2"], "value 2", "extract url params: key2" );
+
+    params = $.extractParams($("#url_with_params_noescape").attr("href"));
+    equal( params["key"], "value", "url from dom: key" );
+    equal( params["noequals"], undefined, "url from dom: noequals" );
+    equal( params["novalue"], "", "url from dom: novalue" );
+    equal( params["key2"], "value 2", "url from dom: key2" );
+
+    params = $.extractParams($("#url_with_params_escaped").attr("href"));
+    equal( params["key"], "value", "url from dom, escaped: key" );
+    equal( params["noequals"], undefined, "url from dom, escaped: noequals" );
+    equal( params["novalue"], "", "url from dom, escaped: novalue" );
+    equal( params["key2"], "value 2", "url from dom, escaped: key2" );
+});
--------------------------------------------------------------------------------