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" );
+});
--------------------------------------------------------------------------------

Post a comment in response:

This account has disabled anonymous posting.
If you don't have an account you can create one now.
HTML doesn't work in the subject.
More info about formatting

If you are unable to use this captcha for any reason, please contact us by email at support@dreamwidth.org