r60073 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r60072‎ | r60073 | r60074 >
Date:12:02, 15 December 2009
Author:werdna
Status:deferred
Tags:
Comment:
LiquidThreads: Add drag-and-drop refactoring of threads.
* Users can now click on "Drag to a new location" in the menu, and drag the post to a new location (this allows reordering and rearranging the hierarchy of threads). Replaces (with graceful fallback) the old interface for splitting and merging.
* Includes a confirmation dialog.
TODO: Vandal resistance, graceful non-JS fallback for reordering threads.
Modified paths:
  • /trunk/extensions/LiquidThreads/LiquidThreads.php (modified) (history)
  • /trunk/extensions/LiquidThreads/api/ApiThreadAction.php (modified) (history)
  • /trunk/extensions/LiquidThreads/classes/Thread.php (modified) (history)
  • /trunk/extensions/LiquidThreads/classes/ThreadHistoryPager.php (modified) (history)
  • /trunk/extensions/LiquidThreads/classes/Threads.php (modified) (history)
  • /trunk/extensions/LiquidThreads/classes/View.php (modified) (history)
  • /trunk/extensions/LiquidThreads/i18n/Lqt.i18n.php (modified) (history)
  • /trunk/extensions/LiquidThreads/jquery/js2.combined.js (modified) (history)
  • /trunk/extensions/LiquidThreads/lqt.css (modified) (history)
  • /trunk/extensions/LiquidThreads/lqt.js (modified) (history)

Diff [purge]

Index: trunk/extensions/LiquidThreads/i18n/Lqt.i18n.php
@@ -60,6 +60,7 @@
6161 'lqt_hist_merged_to' => '[[$1|Reply]] moved from another thread',
6262 'lqt_hist_split_from' => 'Split to a new thread',
6363 'lqt_hist_root_blanked' => 'Removed comment text',
 64+ 'lqt_hist_adjusted_sortkey' => 'Adjusted thread position',
6465
6566 'lqt_revision_as_of' => "Revision as of $2 at $3.",
6667
@@ -260,6 +261,17 @@
261262 'lqt-save-subject-error-unknown' => 'An unknown error occurred when attempting '.
262263 'to set the subject of this thread. Please try to do this by clicking "edit" on the top post.',
263264 'lqt-cancel-subject-edit' => 'Cancel',
 265+ 'lqt-drag-activate' => 'Drag to new location',
 266+ 'lqt-drag-drop-zone' => 'Drop here',
 267+ 'lqt-drag-confirm' => 'To complete the following actions, please fill in a reason '.
 268+ 'and click "Confirm".',
 269+ 'lqt-drag-reparent' => "Move post to underneath a new parent.",
 270+ 'lqt-drag-split' => 'Move post to its own thread',
 271+ 'lqt-drag-setsortkey' => "Adjust post's position on the page",
 272+ 'lqt-drag-bump' => 'Move post to top of discussion page',
 273+ 'lqt-drag-save' => 'Confirm',
 274+ 'lqt-drag-reason' => 'Reason for change: ',
 275+ 'lqt-drag-subject' => 'Subject for new thread: ',
264276
265277 // Feeds
266278 'lqt-feed-title-all' => '{{SITENAME}} — New posts',
Index: trunk/extensions/LiquidThreads/jquery/js2.combined.js
@@ -5659,7 +5659,291 @@
56605660 });
56615661
56625662 })(jQuery);
 5663+
56635664 /*
 5665+ * jQuery UI Droppable 1.7.2
 5666+ *
 5667+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
 5668+ * Dual licensed under the MIT (MIT-LICENSE.txt)
 5669+ * and GPL (GPL-LICENSE.txt) licenses.
 5670+ *
 5671+ * http://docs.jquery.com/UI/Droppables
 5672+ *
 5673+ * Depends:
 5674+ * ui.core.js
 5675+ * ui.draggable.js
 5676+ */
 5677+(function($) {
 5678+
 5679+$.widget("ui.droppable", {
 5680+
 5681+ _init: function() {
 5682+
 5683+ var o = this.options, accept = o.accept;
 5684+ this.isover = 0; this.isout = 1;
 5685+
 5686+ this.options.accept = this.options.accept && $.isFunction(this.options.accept) ? this.options.accept : function(d) {
 5687+ return d.is(accept);
 5688+ };
 5689+
 5690+ //Store the droppable's proportions
 5691+ this.proportions = { width: this.element[0].offsetWidth, height: this.element[0].offsetHeight };
 5692+
 5693+ // Add the reference and positions to the manager
 5694+ $.ui.ddmanager.droppables[this.options.scope] = $.ui.ddmanager.droppables[this.options.scope] || [];
 5695+ $.ui.ddmanager.droppables[this.options.scope].push(this);
 5696+
 5697+ (this.options.addClasses && this.element.addClass("ui-droppable"));
 5698+
 5699+ },
 5700+
 5701+ destroy: function() {
 5702+ var drop = $.ui.ddmanager.droppables[this.options.scope];
 5703+ for ( var i = 0; i < drop.length; i++ )
 5704+ if ( drop[i] == this )
 5705+ drop.splice(i, 1);
 5706+
 5707+ this.element
 5708+ .removeClass("ui-droppable ui-droppable-disabled")
 5709+ .removeData("droppable")
 5710+ .unbind(".droppable");
 5711+ },
 5712+
 5713+ _setData: function(key, value) {
 5714+
 5715+ if(key == 'accept') {
 5716+ this.options.accept = value && $.isFunction(value) ? value : function(d) {
 5717+ return d.is(value);
 5718+ };
 5719+ } else {
 5720+ $.widget.prototype._setData.apply(this, arguments);
 5721+ }
 5722+
 5723+ },
 5724+
 5725+ _activate: function(event) {
 5726+ var draggable = $.ui.ddmanager.current;
 5727+ if(this.options.activeClass) this.element.addClass(this.options.activeClass);
 5728+ (draggable && this._trigger('activate', event, this.ui(draggable)));
 5729+ },
 5730+
 5731+ _deactivate: function(event) {
 5732+ var draggable = $.ui.ddmanager.current;
 5733+ if(this.options.activeClass) this.element.removeClass(this.options.activeClass);
 5734+ (draggable && this._trigger('deactivate', event, this.ui(draggable)));
 5735+ },
 5736+
 5737+ _over: function(event) {
 5738+
 5739+ var draggable = $.ui.ddmanager.current;
 5740+ if (!draggable || (draggable.currentItem || draggable.element)[0] == this.element[0]) return; // Bail if draggable and droppable are same element
 5741+
 5742+ if (this.options.accept.call(this.element[0],(draggable.currentItem || draggable.element))) {
 5743+ if(this.options.hoverClass) this.element.addClass(this.options.hoverClass);
 5744+ this._trigger('over', event, this.ui(draggable));
 5745+ }
 5746+
 5747+ },
 5748+
 5749+ _out: function(event) {
 5750+
 5751+ var draggable = $.ui.ddmanager.current;
 5752+ if (!draggable || (draggable.currentItem || draggable.element)[0] == this.element[0]) return; // Bail if draggable and droppable are same element
 5753+
 5754+ if (this.options.accept.call(this.element[0],(draggable.currentItem || draggable.element))) {
 5755+ if(this.options.hoverClass) this.element.removeClass(this.options.hoverClass);
 5756+ this._trigger('out', event, this.ui(draggable));
 5757+ }
 5758+
 5759+ },
 5760+
 5761+ _drop: function(event,custom) {
 5762+
 5763+ var draggable = custom || $.ui.ddmanager.current;
 5764+ if (!draggable || (draggable.currentItem || draggable.element)[0] == this.element[0]) return false; // Bail if draggable and droppable are same element
 5765+
 5766+ var childrenIntersection = false;
 5767+ this.element.find(":data(droppable)").not(".ui-draggable-dragging").each(function() {
 5768+ var inst = $.data(this, 'droppable');
 5769+ if(inst.options.greedy && $.ui.intersect(draggable, $.extend(inst, { offset: inst.element.offset() }), inst.options.tolerance)) {
 5770+ childrenIntersection = true; return false;
 5771+ }
 5772+ });
 5773+ if(childrenIntersection) return false;
 5774+
 5775+ if(this.options.accept.call(this.element[0],(draggable.currentItem || draggable.element))) {
 5776+ if(this.options.activeClass) this.element.removeClass(this.options.activeClass);
 5777+ if(this.options.hoverClass) this.element.removeClass(this.options.hoverClass);
 5778+ this._trigger('drop', event, this.ui(draggable));
 5779+ return this.element;
 5780+ }
 5781+
 5782+ return false;
 5783+
 5784+ },
 5785+
 5786+ ui: function(c) {
 5787+ return {
 5788+ draggable: (c.currentItem || c.element),
 5789+ helper: c.helper,
 5790+ position: c.position,
 5791+ absolutePosition: c.positionAbs, //deprecated
 5792+ offset: c.positionAbs
 5793+ };
 5794+ }
 5795+
 5796+});
 5797+
 5798+$.extend($.ui.droppable, {
 5799+ version: "1.7.2",
 5800+ eventPrefix: 'drop',
 5801+ defaults: {
 5802+ accept: '*',
 5803+ activeClass: false,
 5804+ addClasses: true,
 5805+ greedy: false,
 5806+ hoverClass: false,
 5807+ scope: 'default',
 5808+ tolerance: 'intersect'
 5809+ }
 5810+});
 5811+
 5812+$.ui.intersect = function(draggable, droppable, toleranceMode) {
 5813+
 5814+ if (!droppable.offset) return false;
 5815+
 5816+ var x1 = (draggable.positionAbs || draggable.position.absolute).left, x2 = x1 + draggable.helperProportions.width,
 5817+ y1 = (draggable.positionAbs || draggable.position.absolute).top, y2 = y1 + draggable.helperProportions.height;
 5818+ var l = droppable.offset.left, r = l + droppable.proportions.width,
 5819+ t = droppable.offset.top, b = t + droppable.proportions.height;
 5820+
 5821+ switch (toleranceMode) {
 5822+ case 'fit':
 5823+ return (l < x1 && x2 < r
 5824+ && t < y1 && y2 < b);
 5825+ break;
 5826+ case 'intersect':
 5827+ return (l < x1 + (draggable.helperProportions.width / 2) // Right Half
 5828+ && x2 - (draggable.helperProportions.width / 2) < r // Left Half
 5829+ && t < y1 + (draggable.helperProportions.height / 2) // Bottom Half
 5830+ && y2 - (draggable.helperProportions.height / 2) < b ); // Top Half
 5831+ break;
 5832+ case 'pointer':
 5833+ var draggableLeft = ((draggable.positionAbs || draggable.position.absolute).left + (draggable.clickOffset || draggable.offset.click).left),
 5834+ draggableTop = ((draggable.positionAbs || draggable.position.absolute).top + (draggable.clickOffset || draggable.offset.click).top),
 5835+ isOver = $.ui.isOver(draggableTop, draggableLeft, t, l, droppable.proportions.height, droppable.proportions.width);
 5836+ return isOver;
 5837+ break;
 5838+ case 'touch':
 5839+ return (
 5840+ (y1 >= t && y1 <= b) || // Top edge touching
 5841+ (y2 >= t && y2 <= b) || // Bottom edge touching
 5842+ (y1 < t && y2 > b) // Surrounded vertically
 5843+ ) && (
 5844+ (x1 >= l && x1 <= r) || // Left edge touching
 5845+ (x2 >= l && x2 <= r) || // Right edge touching
 5846+ (x1 < l && x2 > r) // Surrounded horizontally
 5847+ );
 5848+ break;
 5849+ default:
 5850+ return false;
 5851+ break;
 5852+ }
 5853+
 5854+};
 5855+
 5856+/*
 5857+ This manager tracks offsets of draggables and droppables
 5858+*/
 5859+$.ui.ddmanager = {
 5860+ current: null,
 5861+ droppables: { 'default': [] },
 5862+ prepareOffsets: function(t, event) {
 5863+
 5864+ var m = $.ui.ddmanager.droppables[t.options.scope];
 5865+ var type = event ? event.type : null; // workaround for #2317
 5866+ var list = (t.currentItem || t.element).find(":data(droppable)").andSelf();
 5867+
 5868+ droppablesLoop: for (var i = 0; i < m.length; i++) {
 5869+
 5870+ if(m[i].options.disabled || (t && !m[i].options.accept.call(m[i].element[0],(t.currentItem || t.element)))) continue; //No disabled and non-accepted
 5871+ for (var j=0; j < list.length; j++) { if(list[j] == m[i].element[0]) { m[i].proportions.height = 0; continue droppablesLoop; } }; //Filter out elements in the current dragged item
 5872+ m[i].visible = m[i].element.css("display") != "none"; if(!m[i].visible) continue; //If the element is not visible, continue
 5873+
 5874+ m[i].offset = m[i].element.offset();
 5875+ m[i].proportions = { width: m[i].element[0].offsetWidth, height: m[i].element[0].offsetHeight };
 5876+
 5877+ if(type == "mousedown") m[i]._activate.call(m[i], event); //Activate the droppable if used directly from draggables
 5878+
 5879+ }
 5880+
 5881+ },
 5882+ drop: function(draggable, event) {
 5883+
 5884+ var dropped = false;
 5885+ $.each($.ui.ddmanager.droppables[draggable.options.scope], function() {
 5886+
 5887+ if(!this.options) return;
 5888+ if (!this.options.disabled && this.visible && $.ui.intersect(draggable, this, this.options.tolerance))
 5889+ dropped = this._drop.call(this, event);
 5890+
 5891+ if (!this.options.disabled && this.visible && this.options.accept.call(this.element[0],(draggable.currentItem || draggable.element))) {
 5892+ this.isout = 1; this.isover = 0;
 5893+ this._deactivate.call(this, event);
 5894+ }
 5895+
 5896+ });
 5897+ return dropped;
 5898+
 5899+ },
 5900+ drag: function(draggable, event) {
 5901+
 5902+ //If you have a highly dynamic page, you might try this option. It renders positions every time you move the mouse.
 5903+ if(draggable.options.refreshPositions) $.ui.ddmanager.prepareOffsets(draggable, event);
 5904+
 5905+ //Run through all droppables and check their positions based on specific tolerance options
 5906+
 5907+ $.each($.ui.ddmanager.droppables[draggable.options.scope], function() {
 5908+
 5909+ if(this.options.disabled || this.greedyChild || !this.visible) return;
 5910+ var intersects = $.ui.intersect(draggable, this, this.options.tolerance);
 5911+
 5912+ var c = !intersects && this.isover == 1 ? 'isout' : (intersects && this.isover == 0 ? 'isover' : null);
 5913+ if(!c) return;
 5914+
 5915+ var parentInstance;
 5916+ if (this.options.greedy) {
 5917+ var parent = this.element.parents(':data(droppable):eq(0)');
 5918+ if (parent.length) {
 5919+ parentInstance = $.data(parent[0], 'droppable');
 5920+ parentInstance.greedyChild = (c == 'isover' ? 1 : 0);
 5921+ }
 5922+ }
 5923+
 5924+ // we just moved into a greedy child
 5925+ if (parentInstance && c == 'isover') {
 5926+ parentInstance['isover'] = 0;
 5927+ parentInstance['isout'] = 1;
 5928+ parentInstance._out.call(parentInstance, event);
 5929+ }
 5930+
 5931+ this[c] = 1; this[c == 'isout' ? 'isover' : 'isout'] = 0;
 5932+ this[c == "isover" ? "_over" : "_out"].call(this, event);
 5933+
 5934+ // we just moved out of a greedy child
 5935+ if (parentInstance && c == 'isout') {
 5936+ parentInstance['isout'] = 0;
 5937+ parentInstance['isover'] = 1;
 5938+ parentInstance._over.call(parentInstance, event);
 5939+ }
 5940+ });
 5941+
 5942+ }
 5943+};
 5944+
 5945+})(jQuery);
 5946+
 5947+/*
56645948 * jQuery UI Resizable 1.7.2
56655949 *
56665950 * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
Index: trunk/extensions/LiquidThreads/lqt.css
@@ -469,3 +469,22 @@
470470 .lqt-reply-form {
471471 padding: 0.5em 1em;
472472 }
 473+
 474+.lqt-drop-zone {
 475+ margin-top: 1em;
 476+ margin-bottom: 1em;
 477+ padding: 0.5em 1em;
 478+ border: 1px solid #999999;
 479+ text-align: center center;
 480+ color: #999999;
 481+}
 482+
 483+.lqt-drop-zone.lqt-drop-zone-active {
 484+ border: 1px solid #3333ff !important;
 485+}
 486+
 487+.lqt-drop-zone.lqt-drop-zone-hover {
 488+ border: 2px solid #3333ff !important;
 489+ color: #3333ff !important;
 490+ font-weight: bold !important;
 491+}
Index: trunk/extensions/LiquidThreads/LiquidThreads.php
@@ -204,10 +204,11 @@
205205 $wgLqtEnotif = true;
206206
207207 /* Thread actions which do *not* cause threads to be "bumped" to the top */
208 -/* Using numbers because the change type constants are defined in Thread.php, don't
 208+/* Using numbers because the change type constants are defined in Threads.php, don't
209209 want to have to parse it on every page view */
210210 $wgThreadActionsNoBump = array( 3 /* Edited summary */, 10 /* Merged from */,
211 - 12 /* Split from */, 2 /* Edited root */, );
 211+ 12 /* Split from */, 2 /* Edited root */,
 212+ 14 /* Adjusted sortkey */ );
212213
213214 /** Switch this on if you've migrated from a version before around May 2009 */
214215 $wgLiquidThreadsMigrate = false;
Index: trunk/extensions/LiquidThreads/classes/Threads.php
@@ -23,6 +23,7 @@
2424 const CHANGE_MERGED_TO = 11;
2525 const CHANGE_SPLIT_FROM = 12;
2626 const CHANGE_ROOT_BLANKED = 13;
 27+ const CHANGE_ADJUSTED_SORTKEY = 14;
2728
2829 static $VALID_CHANGE_TYPES = array(
2930 self::CHANGE_EDITED_SUMMARY,
@@ -39,6 +40,7 @@
4041 self::CHANGE_MERGED_TO,
4142 self::CHANGE_SPLIT_FROM,
4243 self::CHANGE_ROOT_BLANKED,
 44+ self::CHANGE_ADJUSTED_SORTKEY,
4345 );
4446
4547 // Possible values of Thread->editedness.
Index: trunk/extensions/LiquidThreads/classes/View.php
@@ -855,6 +855,16 @@
856856 'lqt-ajax-invalid-subject',
857857 'lqt-save-subject-error-unknown',
858858 'lqt-cancel-subject-edit',
 859+ 'lqt-drag-activate',
 860+ 'lqt-drag-drop-zone',
 861+ 'lqt-drag-confirm',
 862+ 'lqt-drag-reparent',
 863+ 'lqt-drag-split',
 864+ 'lqt-drag-setsortkey',
 865+ 'lqt-drag-bump',
 866+ 'lqt-drag-save',
 867+ 'lqt-drag-reason',
 868+ 'lqt-drag-subject',
859869 );
860870
861871 $data = array();
@@ -1454,6 +1464,10 @@
14551465 wfTimestamp( TS_MW, $thread->modified() ),
14561466 array( 'id' => 'lqt-thread-modified-' . $thread->id(),
14571467 'class' => 'lqt-thread-modified' ) );
 1468+ $html .= Xml::hidden( 'lqt-thread-sortkey',
 1469+ $thread->sortkey(),
 1470+ array( 'id' => 'lqt-thread-sortkey-' . $thread->id() )
 1471+ );
14581472 }
14591473
14601474 // Add the thread's title
Index: trunk/extensions/LiquidThreads/classes/Thread.php
@@ -150,7 +150,7 @@
151151 $bump = !in_array( $change_type, $wgThreadActionsNoBump );
152152 }
153153 if ( $bump ) {
154 - $this->sortkey = wfTimestampNow( TS_DB );
 154+ $this->sortkey = wfTimestamp( TS_MW );
155155 }
156156
157157 $this->modified = wfTimestampNow();
@@ -159,7 +159,7 @@
160160
161161 $topmost = $this->topmostThread();
162162 $topmost->modified = wfTimestampNow();
163 - if ( $bump ) $topmost->setSortkey( wfTimestampNow( TS_DB ) );
 163+ if ( $bump ) $topmost->setSortkey( wfTimestamp( TS_MW ) );
164164 $topmost->save();
165165
166166 ThreadRevision::create( $this, $change_type, $change_object, $reason );
@@ -415,7 +415,7 @@
416416 $dbr = wfGetDB( DB_SLAVE );
417417 $this->modified = $dbr->timestamp( wfTimestampNow() );
418418 $this->created = $dbr->timestamp( wfTimestampNow() );
419 - $this->sortkey = wfTimestampNow( TS_DB );
 419+ $this->sortkey = wfTimestamp( TS_MW );
420420 $this->editedness = Threads::EDITED_NEVER;
421421 $this->replyCount = 0;
422422 return;
@@ -868,12 +868,15 @@
869869 foreach( $replies as $reply ) {
870870 if ( ! $reply->hasSuperthread() ) {
871871 throw new MWException( "Post ".$this->id().
872 - " has contaminated reply ".$reply->id()."\n" );
 872+ " has contaminated reply ".$reply->id().
 873+ ". Found no superthread.");
873874 }
874875
875876 if ( $reply->superthread()->id() != $this->id() ) {
876877 throw new MWException( "Post ". $this->id() .
877 - " has contaminated reply ".$reply->id()."\n" );
 878+ " has contaminated reply ".$reply->id().
 879+ ". Expected ".$this->id().", got ".
 880+ $reply->superthread()->id() );
878881 }
879882 }
880883 }
@@ -1292,7 +1295,7 @@
12931296 function setSortKey( $k = null ) {
12941297 if ( is_null( $k ) ) {
12951298 $dbr = wfGetDB( DB_SLAVE );
1296 - $k = wfTimestampNow( TS_DB );
 1299+ $k = wfTimestamp( TS_MW );
12971300 }
12981301
12991302 $this->sortkey = $k;
@@ -1325,7 +1328,7 @@
13261329 }
13271330 }
13281331
1329 - public function split( $newSubject, $reason = '' ) {
 1332+ public function split( $newSubject, $reason = '', $newSortkey = null ) {
13301333 $oldTopThread = $this->topmostThread();
13311334 $oldParent = $this->superthread();
13321335
@@ -1333,15 +1336,21 @@
13341337
13351338 $oldParent->removeReply( $this );
13361339
 1340+ $bump = null;
 1341+ if ( !is_null($newSortkey) ) {
 1342+ $this->setSortkey( $newSortkey );
 1343+ $bump = false;
 1344+ }
 1345+
13371346 $oldTopThread->commitRevision( Threads::CHANGE_SPLIT_FROM, $this, $reason );
1338 - $this->commitRevision( Threads::CHANGE_SPLIT, null, $reason );
 1347+ $this->commitRevision( Threads::CHANGE_SPLIT, null, $reason, $bump );
13391348 }
13401349
13411350 public function moveToParent( $newParent, $reason = '' ) {
13421351 $newSubject = $newParent->subject();
13431352
13441353 $oldTopThread = $newParent->topmostThread();
1345 - $oldParent = $newParent->superthread();
 1354+ $oldParent = $this->superthread();
13461355
13471356 Thread::recursiveSet( $this, $newSubject, $newParent, $newParent );
13481357
Index: trunk/extensions/LiquidThreads/classes/ThreadHistoryPager.php
@@ -25,6 +25,7 @@
2626 Threads::CHANGE_MERGED_TO => wfMsgNoTrans( 'lqt_hist_merged_to' ),
2727 Threads::CHANGE_SPLIT_FROM => wfMsgNoTrans( 'lqt_hist_split_from' ),
2828 Threads::CHANGE_ROOT_BLANKED => wfMsgNoTrans( 'lqt_hist_root_blanked' ),
 29+ Threads::CHANGE_ADJUSTED_SORTKEY => wfMsgNoTrans( 'lqt_hist_adjusted_sortkey' ),
2930 );
3031 }
3132
Index: trunk/extensions/LiquidThreads/api/ApiThreadAction.php
@@ -12,9 +12,10 @@
1313 'markunread' => 'actionMarkUnread',
1414 'split' => 'actionSplit',
1515 'merge' => 'actionMerge',
16 - 'reply' => 'actionReply', // Not implemented
 16+ 'reply' => 'actionReply',
1717 'newthread' => 'actionNewThread',
1818 'setsubject' => 'actionSetSubject',
 19+ 'setsortkey' => 'actionSetSortkey',
1920 );
2021 }
2122
@@ -35,6 +36,9 @@
3637 "timestamp. If false, does not set it. Default depends on ".
3738 "the action being taken. Presently only works for newthread ".
3839 "and reply actions.",
 40+ 'sortkey' => "Specifies the timestamp to which to set a thread's ".
 41+ "sort key. Must be in the form YYYYMMddhhmmss, ".
 42+ "a unix timestamp or 'now'.",
3943 );
4044 }
4145
@@ -60,6 +64,7 @@
6165 'text' => null,
6266 'render' => null,
6367 'bump' => null,
 68+ 'sortkey' => null,
6469 );
6570 }
6671
@@ -192,8 +197,17 @@
193198 $reason = $params['reason'];
194199 }
195200
 201+ // Check if they specified a sortkey
 202+ $sortkey = null;
 203+ if ( !empty($params['sortkey']) ) {
 204+ $ts = $params['sortkey'];
 205+ $ts = wfTimestamp( TS_MW, $ts );
 206+
 207+ $sortkey = $ts;
 208+ }
 209+
196210 // Do the split
197 - $thread->split( $subject, $reason );
 211+ $thread->split( $subject, $reason, $sortkey );
198212
199213 $result = array();
200214 $result[] =
@@ -576,7 +590,7 @@
577591 'action' => 'setsubject',
578592 'result' => 'success',
579593 'thread-id' => $thread->id(),
580 - 'thread-title' => $thread->title(),
 594+ 'thread-title' => $thread->title()->getPrefixedText(),
581595 'new-subject' => $subject,
582596 );
583597
@@ -585,6 +599,58 @@
586600 $this->getResult()->addValue( null, $this->getModuleName(), $result );
587601 }
588602
 603+ public function actionSetSortkey( $threads, $params ) {
 604+ // First check for threads
 605+ if ( !count($threads) ) {
 606+ $this->dieUsage( 'You must specify a thread to set the sortkey of',
 607+ 'no-specified-threads' );
 608+ return;
 609+ }
 610+
 611+ // Validate timestamp
 612+ if ( empty( $params['sortkey'] ) ) {
 613+ $this->dieUsage( 'You must specify a valid timestamp for the sortkey'.
 614+ 'parameter. It should be in the form YYYYMMddhhmmss, a '.
 615+ 'unix timestamp or "now".', 'invalid-sortkey' );
 616+ return;
 617+ }
 618+
 619+ $ts = $params['sortkey'];
 620+
 621+ if ($ts == 'now') $ts = wfTimestampNow();
 622+
 623+ $ts = wfTimestamp( TS_MW, $ts );
 624+
 625+ if ( !$ts ) {
 626+ $this->dieUsage( 'You must specify a valid timestamp for the sortkey'.
 627+ 'parameter. It should be in the form YYYYMMddhhmmss, a '.
 628+ 'unix timestamp or "now".', 'invalid-sortkey' );
 629+ return;
 630+ }
 631+
 632+ $reason = null;
 633+
 634+ if ( isset( $params['reason'] ) ) {
 635+ $reason = $params['reason'];
 636+ }
 637+
 638+ $thread = array_pop($threads);
 639+ $thread->setSortkey( $ts );
 640+ $thread->commitRevision( Threads::CHANGE_ADJUSTED_SORTKEY, null, $reason );
 641+
 642+ $result = array(
 643+ 'action' => 'setsortkey',
 644+ 'result' => 'success',
 645+ 'thread-id' => $thread->id(),
 646+ 'thread-title' => $thread->title()->getPrefixedText(),
 647+ 'new-sortkey' => $ts,
 648+ );
 649+
 650+ $result = array( 'thread' => $result );
 651+
 652+ $this->getResult()->addValue( null, $this->getModuleName(), $result );
 653+ }
 654+
589655 public function getVersion() {
590656 return __CLASS__ . ': $Id: $';
591657 }
Index: trunk/extensions/LiquidThreads/lqt.js
@@ -35,14 +35,7 @@
3636
3737 // Try to find a place for it
3838 if ( !repliesElement.length ) {
39 - repliesElement = $j('<div class="lqt-thread-replies"/>' );
40 -
41 - var finishDiv = $j('<div class="lqt-replies-finish"/>');
42 - finishDiv.append($j('<div class="lqt-replies-finish-corner"/>'));
43 - finishDiv.contents().html('&nbsp;');
44 - repliesElement.append(finishDiv);
45 -
46 - $j(container).append(repliesElement);
 39+ repliesElement = liquidThreads.getRepliesElement( $j(container) );
4740 }
4841
4942 repliesElement.find('.lqt-replies-finish').before( replyDiv );
@@ -55,6 +48,38 @@
5649 liquidThreads.currentReplyThread = thread_id;
5750 },
5851
 52+ 'getRepliesElement' : function(thread /* a .lqt_thread */ ) {
 53+ var repliesElement = thread.contents().filter('.lqt-thread-replies');
 54+
 55+ if ( !repliesElement.length ) {
 56+ repliesElement = $j('<div class="lqt-thread-replies"/>' );
 57+
 58+ var finishDiv = $j('<div class="lqt-replies-finish"/>');
 59+ finishDiv.append($j('<div class="lqt-replies-finish-corner"/>'));
 60+ finishDiv.contents().html('&nbsp;');
 61+ repliesElement.append(finishDiv);
 62+
 63+ var repliesFinishElement = thread.contents().filter('.lqt-replies-finish');
 64+ if ( repliesFinishElement.length ) {
 65+ repliesFinishElement.before(repliesElement);
 66+ } else {
 67+ thread.append(repliesElement);
 68+ }
 69+ }
 70+
 71+ return repliesElement;
 72+ },
 73+
 74+ 'checkEmptyReplies' : function( element ) {
 75+ var contents = element.contents();
 76+
 77+ contents = contents.not('.lqt-replies-finish,.lqt-post-sep,.lqt-edit-form');
 78+
 79+ if ( !contents.length ) {
 80+ element.remove();
 81+ }
 82+ },
 83+
5984 'handleNewLink' : function(e) {
6085 e.preventDefault();
6186
@@ -264,23 +289,18 @@
265290 },
266291
267292 'cancelEdit' : function( e ) {
268 - if (e.preventDefault) {
 293+ if ( typeof e != 'undefined' && typeof e.preventDefault == 'function' ) {
269294 e.preventDefault();
270295 }
271296
272297 $j('.lqt-edit-form').not(e).each(
273298 function() {
274299 var repliesElement = $j(this).closest('.lqt-thread-replies');
275 - var emptyAll = repliesElement.length &&
276 - !( repliesElement.contents().not('.lqt-replies-finish')
277 - .not('.lqt-edit-form').length );
278300 $j(this).fadeOut('slow',
279301 function() {
280302 $j(this).empty();
281303
282 - if (emptyAll) {
283 - repliesElement.remove();
284 - }
 304+ liquidThreads.checkEmptyReplies( repliesElement );
285305 } )
286306 } );
287307
@@ -305,6 +325,18 @@
306326 var replyLink = menu.find('.lqt-command-reply > a');
307327 replyLink.data( 'thread-id', threadID );
308328 replyLink.click( liquidThreads.handleReplyLink );
 329+
 330+ // Add "Drag to new location" to menu
 331+ var dragLI = $j('<li class="lqt-command-drag" />' );
 332+ var dragLink = $j('<a/>').text( wgLqtMessages['lqt-drag-activate'] );
 333+ dragLink.attr('href','#');
 334+ dragLI.append(dragLink);
 335+ dragLink.click( liquidThreads.activateDragDrop );
 336+
 337+ menu.append(dragLI);
 338+
 339+ // Remove split and merge
 340+ menu.contents().filter('.lqt-command-split,.lqt-command-merge').remove();
309341
310342 var trigger = menuContainer.find( '.lqt-thread-actions-trigger' )
311343
@@ -536,7 +568,7 @@
537569 'doReloadThread' : function( thread /* The .lqt_thread */ ) {
538570 var post = thread.find('div.lqt-post-wrapper')[0];
539571 post = $j(post);
540 - var threadId = post.data('thread-id');
 572+ var threadId = thread.data('thread-id');
541573 var loader = $j('<div class="mw-ajax-loader"/>');
542574 var header = $j('#lqt-header-'+threadId);
543575
@@ -559,14 +591,20 @@
560592 header.hide();
561593
562594 // Replace post content
563 - var newThread = newContent.filter('div.lqt_thread')[0];
564 - var newThreadContent = $j(newThread).contents();
 595+ var newThread = newContent.filter('div.lqt_thread');
 596+ var newThreadContent = newThread.contents();
565597 thread.append( newThreadContent );
 598+ thread.attr( 'class', newThread.attr('class') );
566599
567600 // Replace header content
568601 var newHeader = newContent.filter('#lqt-header-'+threadId);
569 - var newHeaderContent = $j(newHeader).contents();
570 - header.append( newHeaderContent );
 602+ if ( header.length ) {
 603+ var newHeaderContent = $j(newHeader).contents();
 604+ header.append( newHeaderContent );
 605+ } else {
 606+ // No existing header, add one before the thread
 607+ thread.before(newHeader);
 608+ }
571609
572610 // Set up thread.
573611 thread.find('.lqt-post-wrapper').each( function() {
@@ -591,6 +629,8 @@
592630 var threadId = threadWrapper.id.substring( prefixLength );
593631
594632 $j(threadContainer).data( 'thread-id', threadId );
 633+ $j(threadWrapper).data( 'thread-id', threadId );
 634+ console.log( "Set up thread "+threadId );
595635
596636 // Set up reply link
597637 var replyLinks = $j(threadWrapper).find('.lqt-add-reply');
@@ -1041,6 +1081,389 @@
10421082 }
10431083 }, 'json' );
10441084 } );
 1085+ },
 1086+
 1087+ 'activateDragDrop' : function(e) {
 1088+ e.preventDefault();
 1089+
 1090+ // Set up draggability.
 1091+ var thread = $j(this).closest('.lqt_thread');
 1092+ var threadID = thread.find('.lqt-post-wrapper').data('thread-id');
 1093+
 1094+ var helperFunc;
 1095+ if ( thread.hasClass( 'lqt-thread-topmost' ) ) {
 1096+ var header = $j('#lqt-header-'+threadID);
 1097+ var headline = header.contents().filter('.mw-headline').clone();
 1098+ var helper = $j('<h2/>').append(headline);
 1099+ helperFunc = function() { return helper; };
 1100+ } else {
 1101+ helperFunc =
 1102+ function() {
 1103+ var helper = thread.clone();
 1104+ helper.find('.lqt-thread-replies').remove();
 1105+ return helper;
 1106+ };
 1107+ }
 1108+
 1109+ var draggableOptions =
 1110+ {
 1111+ 'axis' : 'y',
 1112+ 'opacity' : '0.70',
 1113+ 'revert' : 'invalid',
 1114+ 'helper' : helperFunc
 1115+ };
 1116+ thread.draggable( draggableOptions );
 1117+
 1118+ // Kill all existing drop zones
 1119+ $j('.lqt-drop-zone').remove();
 1120+
 1121+ // Set up some dropping targets. Add one before the first thread, after every
 1122+ // other thread, and as a subthread of every post.
 1123+ var createDropZone = function( ) {
 1124+ var element = $j('<div class="lqt-drop-zone" />');
 1125+ element.text( wgLqtMessages['lqt-drag-drop-zone'] );
 1126+ return element;
 1127+ };
 1128+
 1129+ // First drop zone
 1130+ var firstDropZone = createDropZone();
 1131+ firstDropZone.data( 'sortkey', 'now' );
 1132+ firstDropZone.data( 'parent', 'top' );
 1133+ var firstThread = $j('.lqt-thread-topmost.lqt-thread-first');
 1134+ var firstThreadID = firstThread.find('.lqt-post-wrapper').data('thread-id');
 1135+ var firstHeading = $j('#lqt-header-'+firstThreadID);
 1136+ firstHeading.before(firstDropZone);
 1137+
 1138+ // Now one after every thread
 1139+ $j('.lqt-thread-topmost').each( function() {
 1140+ var sortkeySelector = 'input[name=lqt-thread-sortkey]';
 1141+ var sortkeyField = $j(this).contents().filter(sortkeySelector);
 1142+ var sortkey = parseInt(sortkeyField.val());
 1143+
 1144+ var dropZone = createDropZone();
 1145+ dropZone.data( 'sortkey', sortkey - 1 );
 1146+ dropZone.data( 'parent', 'top' );
 1147+ $j(this).after(dropZone);
 1148+ } );
 1149+
 1150+ // Now one underneath every thread
 1151+ $j('.lqt_thread').each( function() {
 1152+ var thread = $j(this);
 1153+ var repliesElement = liquidThreads.getRepliesElement( thread );
 1154+ var dropZone = createDropZone();
 1155+ var threadId = thread.data('thread-id');
 1156+
 1157+ dropZone.data( 'sortkey', 'now' );
 1158+ dropZone.data( 'parent', threadId );
 1159+
 1160+ repliesElement.contents().filter('.lqt-replies-finish').before(dropZone);
 1161+
 1162+ } );
 1163+
 1164+ var droppableOptions =
 1165+ {
 1166+ 'activeClass' : 'lqt-drop-zone-active',
 1167+ 'hoverClass' : 'lqt-drop-zone-hover',
 1168+ 'drop' : liquidThreads.completeDragDrop,
 1169+ 'tolerance' : 'intersect'
 1170+ };
 1171+
 1172+ $j('.lqt-drop-zone').droppable( droppableOptions );
 1173+ },
 1174+
 1175+ 'completeDragDrop' : function( e, ui ) {
 1176+ var thread = $j(ui.draggable);
 1177+
 1178+ // Determine parameters
 1179+ var params = {
 1180+ 'sortkey' : $j(this).data('sortkey'),
 1181+ 'parent' : $j(this).data('parent')
 1182+ };
 1183+
 1184+ // Figure out an insertion point
 1185+ if ( $j(this).prev().length ) {
 1186+ params.insertAfter = $j(this).prev();
 1187+ } else if ( $j(this).next().length ) {
 1188+ params.insertBefore = $j(this).next();
 1189+ } else {
 1190+ params.insertUnder = $j(this).parent();
 1191+ }
 1192+
 1193+ // Kill the helper.
 1194+ ui.helper.remove();
 1195+
 1196+ setTimeout( function() { thread.draggable('destroy'); }, 1 );
 1197+
 1198+ // Now, let's do our updates
 1199+ liquidThreads.confirmDragDrop( thread, params );
 1200+
 1201+ $j('.lqt-drop-zone').each( function() {
 1202+ var repliesHolder = $j(this).closest('.lqt-thread-replies');
 1203+
 1204+ $j(this).remove();
 1205+
 1206+ if (repliesHolder.length) {
 1207+ liquidThreads.checkEmptyReplies(repliesHolder);
 1208+ }
 1209+ } );
 1210+ },
 1211+
 1212+ 'confirmDragDrop' : function( thread, params ) {
 1213+ var confirmDialog = $j('<div class="lqt-drag-confirm" />');
 1214+
 1215+ // Add an intro
 1216+ var intro = $j('<p/>').text( wgLqtMessages['lqt-drag-confirm'] );
 1217+ confirmDialog.append( intro );
 1218+
 1219+ // Summarize changes to be made
 1220+ var actionSummary = $j('<ul/>');
 1221+
 1222+ var addAction = function(msg) {
 1223+ var li = $j('<li/>');
 1224+ li.text( wgLqtMessages[msg] );
 1225+ actionSummary.append(li);
 1226+ };
 1227+
 1228+ var bump = (params.sortkey == 'now');
 1229+ var topLevel = (params.parent == 'top');
 1230+ var wasTopLevel = thread.hasClass( 'lqt-thread-topmost' );
 1231+
 1232+ if ( params.sortkey == 'now' && wasTopLevel && topLevel ) {
 1233+ addAction( 'lqt-drag-bump' );
 1234+ } else if ( topLevel && params.sortkey != 'now' ) {
 1235+ addAction( 'lqt-drag-setsortkey' );
 1236+ }
 1237+
 1238+ if ( !wasTopLevel && topLevel ) {
 1239+ addAction( 'lqt-drag-split' );
 1240+ } else if ( !topLevel ) {
 1241+ addAction( 'lqt-drag-reparent' );
 1242+ }
 1243+
 1244+ confirmDialog.append(actionSummary);
 1245+
 1246+ // Summary prompt
 1247+ var summaryPrompt = $j('<p/>').text( wgLqtMessages['lqt-drag-reason'] );
 1248+ var summaryField = $j('<input type="text" size="45"/>');
 1249+ summaryField.addClass( 'lqt-drag-confirm-reason' ).attr('name', 'reason');
 1250+ summaryPrompt.append( summaryField );
 1251+ confirmDialog.append( summaryPrompt );
 1252+
 1253+ if ( typeof params.reason != 'undefined' ) {
 1254+ summaryField.val(params.reason);
 1255+ }
 1256+
 1257+ // New subject prompt, if appropriate
 1258+ if ( !wasTopLevel && topLevel ) {
 1259+ var subjectPrompt = $j('<p/>').text( wgLqtMessages['lqt-drag-subject'] );
 1260+ var subjectField = $j('<input type="text" size="45"/>');
 1261+ subjectField.addClass( 'lqt-drag-confirm-subject' )
 1262+ .attr( 'name', 'subject' );
 1263+ subjectPrompt.append( subjectField );
 1264+ confirmDialog.append( subjectPrompt );
 1265+ }
 1266+
 1267+ // Now dialogify it.
 1268+ $j('body').append(confirmDialog);
 1269+
 1270+ var spinner;
 1271+ var successCallback = function() {
 1272+ confirmDialog.dialog('close');
 1273+ confirmDialog.remove();
 1274+ spinner.remove();
 1275+ liquidThreads.reloadTOC();
 1276+ };
 1277+
 1278+ var buttonLabel = wgLqtMessages['lqt-drag-save']
 1279+ var buttons = {};
 1280+ buttons[buttonLabel] =
 1281+ function() {
 1282+ // Load data
 1283+ params.reason = $j(this).find('input[name=reason]').val();
 1284+
 1285+ if ( !wasTopLevel && topLevel ) {
 1286+ params.subject =
 1287+ $j(this).find('input[name=subject]').val();
 1288+ }
 1289+
 1290+ // Add spinners
 1291+ spinner = $j('<div class="mw-ajax-loader" />');
 1292+ thread.before(spinner)
 1293+
 1294+ if ( typeof params.insertAfter != 'undefined' ) {
 1295+ params.insertAfter.after(spinner);
 1296+ }
 1297+
 1298+ $j(this).dialog('close');
 1299+
 1300+ liquidThreads.submitDragDrop( thread, params,
 1301+ successCallback );
 1302+ };
 1303+ confirmDialog.dialog( { 'AutoOpen' : true, 'buttons' : buttons,
 1304+ 'modal' : true } );
 1305+ },
 1306+
 1307+ 'submitDragDrop' : function( thread, params, callback ) {
 1308+ var newSortkey = params.sortkey;
 1309+ var newParent = params.parent;
 1310+ var threadId = thread.find('.lqt-post-wrapper').data('thread-id');
 1311+
 1312+ var bump = (params.sortkey == 'now');
 1313+ var topLevel = (newParent == 'top');
 1314+ var wasTopLevel = thread.hasClass( 'lqt-thread-topmost' );
 1315+
 1316+ var doneCallback =
 1317+ function(data) {
 1318+ // TODO error handling
 1319+ var result;
 1320+ result = 'success';
 1321+
 1322+ if (typeof data == 'undefined' || !data ||
 1323+ typeof data.threadaction == 'undefined' ) {
 1324+ result = 'failure';
 1325+ }
 1326+
 1327+ if (typeof data.error != 'undefined') {
 1328+ result = data.error.code+': '+data.error.description;
 1329+ }
 1330+
 1331+ if (result != 'success') {
 1332+ alert( "Error: "+result );
 1333+ return;
 1334+ }
 1335+
 1336+ var payload;
 1337+ if ( typeof data.threadaction.thread != 'undefined' ) {
 1338+ payload = data.threadaction.thread;
 1339+ } else if (typeof data.threadaction[0] != 'undefined') {
 1340+ payload = data.threadaction[0];
 1341+ }
 1342+
 1343+ var oldParent = undefined;
 1344+ if (!wasTopLevel) {
 1345+ oldParent = thread.closest('.lqt-thread-topmost');
 1346+ }
 1347+
 1348+ // Do the actual physical movement
 1349+ var threadId = thread.find('.lqt-post-wrapper')
 1350+ .data('thread-id');
 1351+ var topmost = thread.hasClass('lqt-thread-topmost');
 1352+
 1353+ if ( topmost ) {
 1354+ var heading = $j('#lqt-header-'+threadId);
 1355+ }
 1356+
 1357+
 1358+ // Assorted ways of returning a thread to its proper place.
 1359+ if ( typeof params.insertAfter != 'undefined' ) {
 1360+ // Move the heading
 1361+ if ( topmost ) {
 1362+ heading.remove();
 1363+ params.insertAfter.after(heading);
 1364+ thread.remove();
 1365+ heading.after( thread );
 1366+ } else {
 1367+ thread.remove();
 1368+ params.insertAfter.after(thread);
 1369+ }
 1370+ } else if ( typeof params.insertBefore != 'undefined' ) {
 1371+ if ( topmost ) {
 1372+ heading.remove();
 1373+ params.insertBefore.before(heading);
 1374+ thread.remove();
 1375+ heading.after( thread );
 1376+ } else {
 1377+ thread.remove();
 1378+ params.insertBefore.before( thread );
 1379+ }
 1380+ } else if ( typeof params.insertUnder != 'undefined' ) {
 1381+ if ( topmost ) {
 1382+ heading.remove();
 1383+ params.insertUnder.prepend(heading);
 1384+ thread.remove();
 1385+ heading.after(thread);
 1386+ } else {
 1387+ thread.remove();
 1388+ params.insertUnder.prepend(thread);
 1389+ }
 1390+ }
 1391+
 1392+ thread.data('thread-id', threadId);
 1393+ thread.find('.lqt-post-wrapper').data('thread-id', threadId);
 1394+
 1395+ if ( typeof payload['new-sortkey']
 1396+ != 'undefined') {
 1397+ newSortKey = payload['new-sortkey'];
 1398+ thread.find('.lqt-thread-modified').val( newSortKey );
 1399+ thread.find('input[name=lqt-thread-sortkey]').val(newSortKey);
 1400+ } else {
 1401+ // Force an update on the top-level thread
 1402+ var reloadThread = thread;
 1403+
 1404+ if ( ! topLevel && typeof payload['new-ancestor-id']
 1405+ != 'undefined' ) {
 1406+ var ancestorId = payload['new-ancestor-id'];
 1407+ reloadThread =
 1408+ $j('#lqt_thread_id_'+ancestorId);
 1409+ }
 1410+
 1411+ liquidThreads.doReloadThread( reloadThread );
 1412+ }
 1413+
 1414+ // Kill the heading, if there isn't one.
 1415+ if ( !topLevel && wasTopLevel && heading.length ) {
 1416+ heading.remove();
 1417+ }
 1418+
 1419+ if ( !wasTopLevel && typeof oldParent != 'undefined' ) {
 1420+ liquidThreads.doReloadThread( oldParent );
 1421+ }
 1422+
 1423+ // Call callback
 1424+ if ( typeof callback == 'function' ) {
 1425+ callback();
 1426+ }
 1427+ }
 1428+
 1429+ if ( !topLevel || !wasTopLevel ) {
 1430+
 1431+ // Is it a split or a merge
 1432+ var apiRequest =
 1433+ {
 1434+ 'action' : 'threadaction',
 1435+ 'thread' : threadId,
 1436+ 'format' : 'json',
 1437+ 'reason' : params.reason
 1438+ }
 1439+
 1440+ if (topLevel) {
 1441+ apiRequest.threadaction = 'split';
 1442+ apiRequest.subject = params.subject;
 1443+ } else {
 1444+ apiRequest.threadaction = 'merge';
 1445+ apiRequest.newparent = newParent;
 1446+ }
 1447+
 1448+ if ( newSortkey != 'none' ) {
 1449+ apiRequest.sortkey = newSortkey;
 1450+ }
 1451+
 1452+ liquidThreads.apiRequest( apiRequest, doneCallback );
 1453+
 1454+
 1455+ } else if (newSortkey != 'none' ) {
 1456+ var apiRequest =
 1457+ {
 1458+ 'action' : 'threadaction',
 1459+ 'threadaction' : 'setsortkey',
 1460+ 'thread' : threadId,
 1461+ 'sortkey' : newSortkey,
 1462+ 'format' : 'json',
 1463+ 'reason' : params.reason
 1464+ };
 1465+
 1466+ liquidThreads.apiRequest( apiRequest, doneCallback );
 1467+ }
10451468 }
10461469 }
10471470

Status & tagging log