HTML preprocessors can make writing HTML more powerful or convenient. For instance, Markdown is designed to be easier to write and read for text documents and you could write a loop in Pug.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
CSS preprocessors help make authoring CSS easier. All of them offer things like variables and mixins to provide convenient abstractions.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
To get the best cross-browser support, it is a common practice to apply vendor prefixes to CSS properties and values that require them to work. For instance -webkit-
or -moz-
.
We offer two popular choices: Autoprefixer (which processes your CSS server-side) and -prefix-free (which applies prefixes via a script, client-side).
Any URLs added here will be added as <link>
s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
You can also link to another Pen here (use the .css
URL Extension) and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, use the appropriate URL Extension and we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
JavaScript preprocessors can help make authoring JavaScript easier and more convenient.
Babel includes JSX processing.
Any URL's added here will be added as <script>
s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
Search for and use JavaScript packages from npm here. By selecting a package, an import
statement will be added to the top of the JavaScript editor for this package.
Using packages here is powered by esm.sh, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ESM usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
<!-- start codepen -->
<h1>Sortable List Using GreenSock
<a href="https://greensock.com/draggable" target="_blank">
Draggable</a>
</h1>
<p class='note'>
Each tile's content determines its height.
</p>
<p class='note'>
Tiles can be reordered only by dragging their "handle" (the white div)</p>
<div id="list">
<div class="tile" style="height: 40px">
<div class="content">
<div class='scrubber'></div>
1
</div>
</div>
<div class="tile" style="height: 60px">
<div class="content">
<div class='scrubber'></div>
2 <br>
second line
</div>
</div>
<div class="tile" style="height: 80px">
<div class="content">
<div class='scrubber'></div>
3<br/>
</div>
</div>
<div class="tile">
<div class="content">
<div class='scrubber'></div>
4 <br>
second line <br>
third line
</div>
</div>
<div class="tile">
<div class="content">
<div class='scrubber'></div>
5
</div>
</div>
</div>
<ul class='note'>
<li>thanks is due to "OSUblake" whose work was the foundation for this.</li>
</ul>
<!-- end codepen -->
/* (OSUblake's below) */
html {
font-family: RobotoDraft, 'Helvetica Neue', Helvetica, Arial;
}
body {
background: #3A3A4D;
padding: 25px;
}
*, *:before, *:after {
box-sizing: border-box;
}
h1 {
color: #efefef;
font-weight: normal;
font-weight: 300;
margin-bottom: 30px;
}
h1 a {
color: #efefef;
border-bottom: 2px solid #efefef;
text-decoration: none;
padding-bottom: 3px;
}
h1 a:hover {
color: yellowgreen;
border-color: yellowgreen;
}
#list {
background-color: rgba(0, 0, 0, 0.2);
margin-top: 50px;
padding-bottom: 50px; /* TODO: necessary util we figure out how to .. */
padding-top: 50px; /* .. constraint to the Y axis */
width: 300px;
}
.tile {
background-color: yellowgreen;
color: #3A3A4D;
position: relative; /* only in order to make z-index used */
font-weight: bold;
}
/* mine below */
.tile {
clear: both;
/* display: table; not here */
margin-top: 10px;
padding: 0px; /* any padding will screw up height calculation */
width: 300px;
}
.tile:first-child {
/* margin-top: 0; if you do this, when the top piece is no longer
the top piece, it would need fixing in the code */
}
.tile .content { /* this exists only so that we can have padding without messing up height calculations of tile */
padding: 10px;
}
.scrubber {
cursor: ns-resize;
float: right;
background-color: white;
width: 50px;
height: 20px;
}
.note {
color: grey;
transform: matrix(1, 0, -0.1, 1, 0, 0);
}
function SkylabSortableListCore( listElem ) {
/*
# synopsis
this is the "core" implementation of a sortable list, intended to look
and feel like the sorting interface for iPhone's "reminders" app (at least,
its look at feel at this moment). (this design is probably elsewhere too.)
we say "core" because this code itself is not packaged as e.g. an angular
directive or jQuery plugin; but rather it is intended to serve as the
"backend" for such plugins as we create them. this decoupling is by
design, so that we are not anchored to any one framework's ecosystem, but
rather so we are buoyed by all of them; while being insulated from their
occasionally capricous API's.
that said, we chain ourselves lightly to a couple of them:
# requirements
• some particular jQuery version (but only for selectors in a few places)
• some particluar GSAP (for implementation of drag, animations)
# features
(while each of these features is "out there" somewhere already, we haven't
been able to find all of them together in any *one* of our predecessors:)
• one does not merely move the list item by dragging anywhere on the item:
you must drag it by its "handle" (what we call a "scrubber" here).
allows for the whole list to be itself draggable for other purposes.
• multiple such lists may coexist on the same page - variables are
"correctly" scoped.
• GSAP for (sexy-assed) animations.
• each tile may be of non-uniform, arbitrary height - there is no "grid".
the appearance of each tile is not dictated by the plugin, but by the
existing DOM.
• implementation style:
* jquery selectors are not littered throughout the code.
* adds only one constant to the global namespace.
# TO-DO's / wishlist / known issues
• TODO: turn some display constants into options as needed..
• [ other TO-DO's may exist below (indicated with "todo" in all caps) ]
*/
/* # "constants" (and defaults for possible future options)
credit is due: for those below values that govern appearance (e.g
animation), they mostly come from "OSUBlake"'s
[sortable demo] (https://codepen.io/osublake/pen/RNLdpz)
which was insturmental in getting this whole thing started generally.
## coding conventions introduced
• variables with two leading underscores are only used once ever..
*/
var ACHIEVED = true;
var debug = true;
var __shadow2 = "0 6px 10px 0 rgba(0, 0, 0, 0.3), 0 2px 2px 0 rgba(0, 0, 0, 0.2)";
var __moment = 0.2; // ~ cheeky names for amount of time
var waltz = 0.35;
/*
# section 1 - detecting when a drag session should start
a "drag session" may result in zero to many "re-orderings", as the user
drags the moving tile up and down, etc, before releasing it.
the plug-in does not concern itself with whether these intermediate
re-orderings are more significant than the final order at the end of the
drag session. it (TODO) only emits events, one for each new order
(including these intermediate orderings).
the drag session ends "on release", not "on drag end" - we want to know
that a release happened even if the "moving tile" wasn't moved at all.
by design, we hold that there is no state to maintain in between drag
sessions - after tiles go thru their final reordering for the drag
session (or even when no re-ordering occurred), any permanent changes
are reflected in the DOM (perhaps only in the CSS properties) and/or by
events that we emit to handlers, that express the new order.
the cost of the above is that we index all of the items sometimes
redundantly on each touch. we accept this cost for these reasons:
• having any long-running state between drag sessions gives us
more surface area to test.
• outside agents may mutate our model in between drag sessions (for
example, adding or removing items). accomodating this is easier if
we cache nothing.
## concepts introduced
• "drag session"
• "tile": the "physical" dom element that can be moved (or a jQuery
selection around same.)
• "moving tile": the tile being dragged by the user. (note that other
tiles may move during a drag session as a response to
a re-ordering. we call these "displaced tiles".)
• "displaced tiles": see previous bullet. depending on context, does or
does not include the "moving tile".
## coding conventions introduced
• variables & functions that exist only for readability (and not for re-
use per se) have names with one and two leading underscores respectively.
• we are commenting this module more heavily than usual for reasons.
*/
$( '.scrubber', listElem ).mousedown( __onScrubberTouch );
function __onScrubberTouch() {
var scrubber = $( this );
var movingTile;
var ok;
__findMovingTileFromScrubber();
ok && __dragSessionIsNotAlreadyStarted();
ok && __startDragSession( movingTile, scrubber );
function __findMovingTileFromScrubber() {
// find the tile element as a parent of the scrubber element. we allow
// that the "scrubber" and "tile" could be the same element.
var current = scrubber;
do {
if ( current.hasClass( 'tile' ) ) {
var found = current;
break;
}
current = current.parent();
} while ( current.length );
if ( found ) {
movingTile = found;
ok = ACHIEVED;
} else {
ok = error( "parent tile of scrubber not found (no \"tile\" class)" );
}
}
function __dragSessionIsNotAlreadyStarted() {
var drg = Draggable.get( movingTile );
if ( drg ) {
ok = error( "drag already in session? ignoring touch." );
} else {
ok = ACHIEVED;
}
}
}
/*
# section 2 - indexing items and listening for drags
hackily: although it is only the scrubber we use to detect the touch, we
apply Draggable to the whole surrounding tile so that the whole tile
moves in response to user moves; even though conceptually we pretend it
is only the scrubber that is being dragged.
fortunately the collateral damage of this is limited: because the
surrounding tile picks up each of the four kinds of events we care about,
the rest of the code need be none the wiser of this hack.
## waypoint/threshold theory
the central function of this whole thing is to detect when to trigger a
"displacement" (i.e. re-ordering) of the surrounding tiles based on the
movement of the moving tile. we do this thru the use of "thresholds" and
"waypoints".
we imagine that the vertical center of the scrubber of the tile being
dragged forms an imaginary horizontal "cursor". then we imagine that any
above tile and below tile have imaginary lines thru their vertical
centers (this time of the whole tile, not the scrubber) that we call
"thresholds". once (if ever) the cursor "breaches" (hits or passes) one
of these zero, one or two thresholds; this is what triggers a re-ordering.
each time a threshold is breached and reordering is begun, we update our
"waypoint" to reflect the new normal center of the scrubber. we use this
value to determine whether the position of the scrubber at this moment is
above or below its "resting position", so as to determine whether to
test the above or below threshold.
also, with each new waypoint we get two new thresholds, reflecting the
vertical center of any new above and below tile of the moving tile.
## coding conventions introduced
• we add a *trailing* underscore to a variable to differentiate it from
another otherwise same-named variable in a nearby scope.
*/
function __startDragSession( movingTile, scrubber ) {
var itemIndex = __buildItemIndex();
if (itemIndex) {
__listen();
}
function __buildItemIndex(){
var itemIndexAttempt = new __ItemIndex( movingTile, scrubber );
var _ok = itemIndexAttempt.execute();
if (_ok) {
return itemIndexAttempt;
}
}
var killable;
function __listen(){
killable = Draggable.create( movingTile, {
bounds: listElem,
onPress: __onPress,
onDragStart: __onDragStart,
onDrag: __onDrag,
onRelease: __onRelease,
zIndexBoost: false
})[ 0 ];
}
// we only ever care about Y, not X. we don't have to care what the
// coordinate system is (i.e what "0" means); only that it is downwards,
// and that all of this code is using that same system (whatever it is).
var movingItem;
var pointerToScrubberCenterDelta; // a few pixels
var scrubberCenterWaypoint;
function __onPress() {
movingItem = itemIndex.movingItem();
var scrubberCenterY = movingItem.scrubberCenterY();
scrubberCenterWaypoint = scrubberCenterY;
pointerToScrubberCenterDelta = scrubberCenterY - this.pointerY;
reestablishThresholds();
movingItem.whenPress();
}
function __onDragStart() {
// nothing for now..
}
var checkUpperThreshold, checkLowerThreshold, scrubberCenterY;
function __onDrag() {
scrubberCenterY = this.pointerY + pointerToScrubberCenterDelta;
if (scrubberCenterWaypoint > scrubberCenterY) {
checkUpperThreshold();
} else if (scrubberCenterWaypoint < scrubberCenterY) {
checkLowerThreshold();
}
}
function __onRelease(){
stopListening(); // because we create a new draggable for each session
movingItem.snapToIntendedLocation(); // see
}
function reestablishThresholds() {
var prevItem = movingItem.previousItem();
var nextItem = movingItem.nextItem();
if (prevItem) {
var aboveTileCenterY = prevItem.tileCenterY();
checkUpperThreshold = function() {
if (aboveTileCenterY >= scrubberCenterY) {
whenBreached( true );
}
}
} else {
checkUpperThreshold = whenNoThreshold;
}
if (nextItem) {
var belowTileCenterY = nextItem.tileCenterY();
checkLowerThreshold = function() {
if ( belowTileCenterY <= scrubberCenterY ) {
whenBreached( false );
}
}
} else {
checkLowerThreshold = whenNoThreshold;
}
if (debug) {
var a = [];
if (prevItem) {
a.push( "upper t.h: " + aboveTileCenterY );
} else {
a.push( "no upper t.h." );
}
if (nextItem) {
a.push( "lower t.h: " + belowTileCenterY );
} else {
a.push( "no lower t.h." );
}
console.log( a.join(' ') );
}
}
function whenNoThreshold(){}
function whenBreached( isUpper ) {
var _ok = __reorder( isUpper, scrubberCenterY, itemIndex );
if (_ok) {
scrubberCenterWaypoint = movingItem.tileCenterY();
reestablishThresholds();
} else {
error( "reordering not ok?" );
stopListening();
}
}
function stopListening() {
killable.kill();
}
}
/* # section 3 - reordering
1) assemble all displaced items (including the moving item) into an array
that is in the correct order of the items' intended positions. (the
moving item will always be either at the top or bottom of this array
as appropriate.)
2) correct the links of this assembly, taking care to correct links of
any items that were before or after the displaced segment (but not
themselves displaced).
3) streaming along each displaced tile from the (new) top to bottom tile,
calculate a new "top" value for each tile using simple arithmetic
adding to each previous top value the previous tile's height and a
gutter somehow.
*/
function __reorder( breachedUpper, scrubberCenterY, itemIndex ) {
var items = itemIndex.items;
var movingItem = itemIndex.movingItem();
var originalTopItem, originalBottomItem;
var newOrder, origOrder;
__determineNewOrder();
__correctLinks();
var _ok = __calculateNewTopsAndAnimate( newOrder, origOrder, itemIndex );
return _ok;
function __correctLinks() {
/* for N items that are rearranged (where N is at least 2), there are
always N+1 "joints" that need correcting: there is every 2-way joint
between the N items (which is N-1 joints), and a "joint" before and
a "joint" after. when the block of re-arranged items occurs at the
beginning and or end of all the items, special handling is required.
we make a stream function for these participating items, taking the
above special handling into account:
*/
var anyStationaryUpper = originalTopItem.previousItem();
var anyStationaryLower = originalBottomItem.nextItem();
var next, rest;
var body = streamViaMap( streamViaArray( newOrder ), function( id ) {
return items[ id ];
});
if (anyStationaryLower) {
rest = function() {
var x = body();
if (x) {
return x;
} else {
next = function(){ return null; };
return anyStationaryLower;
}
}
} else {
rest = body;
}
if (anyStationaryUpper) {
next = function() {
next = rest;
return anyStationaryUpper;
};
} else {
next = rest;
}
var first = next();
// if the block of rearrangement is anchored to the beginning:
if (!anyStationaryUpper) {
first.previousItemIdentifier = null;
itemIndex.identifierOfHeadItem = first.id;
}
// breaches always involve at last two items
var prev = first;
var curr = next();
// correct each joint from top to bottom
do {
prev.twoWayJoinToNext( curr );
prev = curr;
curr = next();
} while (curr);
// if the block of rearrangement is anchored to the end:
if (!anyStationaryLower) {
prev.nextItemIdentifier = null;
}
// (otherwiswe whatever item used to follow it still follows it)
}
function __determineNewOrder() {
var a = [];
var a_, next, yes;
var origOrd = [];
var curr = movingItem;
if (breachedUpper) { // if you breached the upper threshold
a.push( movingItem.id ); // then first item (new order) is the moving item
// you will go backwards over each previously above item of the moved
// item until you find one that was not passed over by the move.
// because we are going backwards (upwards), we will need to reverse
// these items when they are done so they are top-down.
a_ = []; // a temp array that will be reversed
next = function() {
curr = curr.previousItem();
return curr;
};
yes = function( item ) {
// the item should be displaced if its vert center is below scrubber
return scrubberCenterY <= item.tileCenterY();
};
originalBottomItem = movingItem;
} else {
// since you breached the lower threshold, we will test each next item
// that used to be below the moving piece in order until we find one we
// didn't pass over.
a_ = a; // there is no temp array. write directly to target destination
next = function() {
origOrd.push( curr.id ); // tricky
curr = curr.nextItem();
return curr;
};
yes = function( item ) {
// the item should be displaced if its vert center is above scrubber
return scrubberCenterY >= item.tileCenterY();
};
originalTopItem = movingItem;
}
var item;
while (item = next()) {
if ( yes( item ) ) {
a_.push( item.id );
} else {
break;
}
}
if (breachedUpper) {
originalTopItem = items[ a_[ a_.length - 1 ] ];
// the temp array is in reverse order of the desired order.
// effectively reverse then concat the tmp ary onto destination ary.
var i = a_.length;
while (i--) {
var d = a_[ i ];
a.push( d );
origOrd.push( d );
}
origOrd.push( movingItem.id );
} else {
originalBottomItem = items[ a[ a.length - 1 ] ];
// when you breached lower, moving item is always the last item
a.push( movingItem.id );
}
newOrder = a;
origOrder = origOrd;
} // __determineNewOrder
} // __reorder
function __calculateNewTopsAndAnimate( newOrd, oldOrd, idx ) {
/* calculate and apply a new "top" for every displaced tile (including
the tile that was dragged). using their intended final positions, we
calculate these new tops from the topmost moved tile downwards, using
appropriate addition at each step, taking into account each relevant
height of the above tile and previous "gutter" as necessary.
*/
var items = idx.items;
function f( d ) { return items[ d ]; }
// in the old order for N rearranged tiles, calculate a cached array of
// N-1 "gutters" (the space between adjacent tiles). we do this in a
// separate pass because we have to access the old tops.
var next = streamViaMap( streamViaArray( oldOrd ), f );
var gutters = [];
var prev = next(); // orig top
var curr = next();
var origFirst = prev;
do {
gutters.push( curr.cachedTop - ( prev.cachedTop + prev.height() ) );
prev = curr;
curr = next();
} while (curr);
var nextGutter = streamViaArray( gutters );
next = streamViaMap( streamViaArray( newOrd ), f );
prev = next(); // new top
curr = next();
prev.prevTop = prev.cachedTop;
prev.cachedTop = origFirst.cachedTop; // let this be the last old top we use
prev.whenNewTop();
do {
curr.prevTop = curr.cachedTop;
curr.cachedTop = prev.cachedTop + prev.height() + nextGutter();
curr.whenNewTop(); // animate now (but you could do it later instead)
prev = curr;
curr = next();
} while (curr);
return ACHIEVED;
}
/* # "animation" section
## conventions introduced:
• "method" names with only a single leading underscore are private
to the structure they are defined in.
*/
function __animationMethods( o ) {
o.whenPress = function() {
// intended for the tile that is probably about to be dragged.
var el = this.element();
var tl = new TimelineLite();
this.topBeforeDrag = this.cachedTop;
this.hacky_original_Y_transform =
__Y_transform_of( el.css( 'transform' ) );
// "click" into the closer z-index before you tween
tl.to( el, 0, { zIndex: 1 } );
// tween to be slightly transparent and sligtly smaller, and with shadow
tl.to( el, __moment, {
autoAlpha: 0.75,
boxShadow: __shadow2,
scale : 0.95,
});
this._playAsOnlyTimeline( tl );
};
o.snapToIntendedLocation = function() {
/* the converse of the above method. the item has stopped moving now.
it could be anywhere. get it from where it is to where it needs to be.
## the drift problem <a name='the-drift-problem'></a>
EEK: bear in mind that the tile is now "anywhere" the user dragged
it to, and it needs to go to its intended location. since this tile
is by default "shrunken" (has a scale tranform on it), jQuery's
`position()` method will (reasonably) take this scale into account
when calculating the `position()`. (the "top" of a shrunken element
will be a larger Y value than if the element were full-size, all
other aspects being equal.)
however, we are transforming it back to normal size as we move it.
hence we don't want the scale tranform of the element to interfere
with us getting a "pure" reading of this imaginary normal top hence
we can't use jQuery's `position()` method. SO:
1) we memoize what the Y transform was on the tile right before
we started dragging it.
2) take the Y value delta between where it used to be (before we
started dragging it) and its intended location now (not where
it actually is).
3) apply this delta to the Y transform from (1) (with our code), then
you have the *absolute* (not relative) transform necessary to move
this piece to its intended location (right?).
this sounds complicated, but without this accomodation we have a very
real "drift" problem with each drag session, of by about 2.5% of the
height of the moving tile.
*/
var orig_Y_transform = this.hacky_original_Y_transform
if ( false === orig_Y_transform ) {
error( "fix me - no original Y transform value available." );
} else {
var el = this.element();
var tl = new TimelineLite();
var _cleanDelta = this.cachedTop - this.topBeforeDrag;
var _intended_Y_transform = orig_Y_transform + _cleanDelta;
// to the converse of the above - move it back etc.
tl.to( el, waltz, {
autoAlpha: 1, // 0.75,
boxShadow: 'none', // __shadow2,
scale : 1, //0.95,
x: 0,
y: _intended_Y_transform,
});
// once it is back in place, bump the z-index down to zero so that
// when future tiles are dragged over this one, that tile is closer
tl.to( el, 0, {
zIndex: 0
});
this._playAsOnlyTimeline( tl );
}
};
o.whenNewTop = function() {
/* this default implementation is intended for those tiles that are
displaced but are not the tile being dragged. note that they may be
in the middle of an existing animation when this message is received.
note too that they may have existing transforms on them from previous
moves, which is why we send the translation in relative terms.
*/
// don't incur the cost of calculating the real top unless you have to..
if ( this.timeline ) {
this._stopExistingTimeline();
var _currentTop = this.element().position().top;
var delta = this.cachedTop - _currentTop;
} else {
delta = this.cachedTop - this.prevTop;
}
var tl = new TimelineLite();
var _s = __relativePixelsStringViaDelta( delta );
tl.to( this.element(), waltz, {
y: _s
});
this._playAsOnlyTimeline( tl );
};
o._playAsOnlyTimeline = function( tl ) {
if ( this.timeline ) {
this._stopExistingTimeline();
}
this.timeline = tl;
var me = this;
tl.eventCallback( "onComplete", function() {
me.timeline = null;
});
tl.play();
};
o._stopExistingTimeline = function() {
this.timeline.pause(); // there is no stop() for tweens..
this.timeline = null;
};
/* ### parse our own CSS :(
(see [the drift problem] (#the-drift-problem))
*/
// matrix( scale skew rotate alpha X Y )
var __Y_transform_of = __buildMatrixMatcher( 5 );
function __buildMatrixMatcher( d ) {
var f = function( s ) {
f = __buildMatcher( d, 'matrix' );
return f( s );
};
return function( s ) {
return f( s );
};
}
function __buildMatcher( d, termString ) {
var rx = __buildRegExp( d, termString );
return function( s ) {
var md = rx.exec( s );
if (md) {
return Number( md[ 1 ] );
} else {
return false;
}
};
}
function __buildRegExp( d, s ) {
var a = [ '^' + s + '\\(' ];
var a_ = [];
if ( 0 < d ) {
var i = d;
while (i--) {
a_.push( numberRxs );
}
}
a_.push( '(' + numberRxs + ')' );
var d_ = 5 - d;
if ( 0 < d_ ) {
i = d_;
while (i--) {
a_.push( numberRxs );
}
}
a.push( optionalSpaceRxs );
a.push( a_.join( ',[ ]*' ) );
a.push( optionalSpaceRxs );
a.push( '\\)$' );
return RegExp( a.join( '' ) );
}
var numberRxs = '-?\\d+(?:\\.\\d+)?(?:e-?\\d+)?';
var optionalSpaceRxs = '[ ]*';
};
function __relativePixelsStringViaDelta( delta ) {
if ( 0 > delta ) {
return '-=' + ( -1 * delta ) + 'px';
} else {
return '+=' + ( delta ) + 'px';
}
}
/* # "model" section - for modeling the items
*/
function __ItemIndex( movingTile, scrubber ) {
// items, identifierOfHeadItem, identifierOfMovingItem
this.execute = function() {
var ok = __catalogItems( this, movingTile, scrubber );
ok && ( ok = __sortAndLinkItems( this ) );
ok && debug && console.log( this.description() );
this.execute = null;
return ok;
};
Object.setPrototypeOf( this, __ItemIndexMethods );
}
var __ItemIndexMethods = {
description: function() {
var a = []
var curr = this.headItem();
while (curr) {
a.push( curr.description() );
curr = curr.nextItem();
}
return '(' + a.join( ',' ) + ')'
},
headItem: function() {
return this.items[ this.identifierOfHeadItem ];
},
movingItem: function() {
return this.items[ this.identifierOfMovingItem ];
}
};
function __catalogItems( results, movingTile, scrubber ) {
// we store values that we consider to be "immutable" (in some regard)
// separate from mutable, in case that ends up becoming useful..
var mutables = []; // elements are struct-like
var immutables = []; // parallel with above, elements are object-like
var moving_DOM_element = movingTile[ 0 ];
var next = streamViaArray( listElem.children() );
var itemPrototype = { // defined here because closes around above
description: __describeItemMethod,
tileCenterY: function() {
return this.cachedTop + immutables[ this.id ].radius;
},
radius: function() {
return immutables[ this.id ].radius;
},
height: function() {
return immutables[ this.id ].height;
},
element: function() {
return immutables[ this.id ].element;
}
};
function __lookup( id ) {
if ( null == id ) {
return null;
} else {
return mutables[ id ];
}
}
__linkedListMethods( itemPrototype, __lookup );
__animationMethods( itemPrototype );
var identifierOfMovingItem = null;
// catalog each item when we don't know which is the moving item
var tile;
while ( tile = next() ) {
if ( moving_DOM_element == tile ) {
__addMovingItem();
break;
} else {
addNonMovingItem();
}
}
// because we've found the moving item we don't have to look for it.
while ( tile = next() ) {
addNonMovingItem();
}
function __addMovingItem() {
var d = beginItem();
immutables[ d ].element = movingTile;
finishItem( d );
var item = mutables[ d ]
var __scrubberCenterDepthInItem_ = __scrubberCenterDepthInItem( item );
item.scrubberCenterY = function() {
return item.cachedTop + __scrubberCenterDepthInItem_;
};
item.whenNewTop = function() {
// the tile being dragged does nothing with the
// notification of a new top *at this point*.
}
identifierOfMovingItem = d;
}
function __scrubberCenterDepthInItem( item ) {
/* WARNING - what "top" means is CSS dependant - the below calcuation
asssumes that the tiles are `position: relative`. (they must be so
so that their z-index is honored.) when the tiles were not, the below
reported "top" was a top in our "normal" coordinates.
*/
var _localTop = scrubber.position().top; // when tile is pos:relative.
// you would have to subtract `item.cachedTop` if tile were not.
var _ht = scrubber.height();
return _localTop + ( _ht / 2 );
}
function addNonMovingItem() {
var d = beginItem();
immutables[ d ].element = $( tile );
finishItem( d );
}
function beginItem() {
var d = immutables.length;
immutables[ d ] = {};
mutables[ d ] = { id: d };
return d;
}
function finishItem( d ) {
var im = immutables[ d ];
var mu = mutables[ d ];
var el = im.element;
var h = el.height();
im.height = h;
im.radius = h / 2;
var top = el.position().top;
mu.cachedTop = top;
Object.setPrototypeOf( mu, itemPrototype );
}
if ( null == identifierOfMovingItem ) {
return error( "moving item not found" );
} else {
results.items = mutables;
results.identifierOfMovingItem = identifierOfMovingItem;
return ACHIEVED;
}
} // __catalogItems
function __sortAndLinkItems( self ) { // set identifierOfMovingItem
var items = self.items;
// let "a" be an array of ID's to items, sorted by cachedTop ascending.
// we can't sort the items array itself because item indexes must persist.
var a = mapViaStream( streamViaArray( items ), function( item ) {
return item.id;
});
a.sort( function( d, d_ ){
var top = items[ d ].cachedTop,
top_ = items[ d_ ].cachedTop;
if ( top < top_ ) {
return -1;
} else if ( top > top_ ) {
return 1;
} else {
return 0;
}
});
// doubly-link the items
var next = streamViaMap( streamViaArray( a ), function( id ) {
return items[ id ];
});
var headItem = next();
if (headItem) {
headItem.previousItemIdentifier = null; // aesthetics
var curr = next();
if (curr) {
var prev = headItem;
do {
prev.twoWayJoinToNext( curr );
prev = curr;
curr = next();
} while (curr);
prev.nextItemIdentifier = null; // aesthetics
}
}
if (headItem) {
self.identifierOfHeadItem = headItem.id;
return ACHIEVED;
} else {
return error( "zero items?" );
}
} // __sortAndLinkItems
function __describeItemMethod() {
return "(" + this.id + ":" + this.cachedTop + ")";
}
function __linkedListMethods( o, lookup ) {
o.nextItem = function() {
return lookup( this.nextItemIdentifier );
};
o.previousItem = function() {
return lookup( this.previousItemIdentifier );
};
o.twoWayJoinToAnyPrevious = function( prv ) {
if (prv) {
prv.twoWayJoinToNext( this );
} else {
this.previousItemIdentifier = null;
}
};
o.twoWayJoinToAnyNext = function( nxt ) {
if (nxt) {
this.twoWayJoinToNext( nxt );
} else {
this.nextItemIdentifier = null;
}
};
o.twoWayJoinToNext = function( item ) {
item.previousItemIdentifier = this.id;
this.nextItemIdentifier = item.id;
};
};
// # support - "streams": null always indicates the end of the stream
function mapViaStream( next, f ) {
var a = [];
var next_ = streamViaMap( next, f );
var curr = next_();
while ( null !== curr ) {
a.push( curr );
curr = next_();
}
return a;
}
function streamViaMap( next, f ) {
return function() {
var curr = next();
if ( null === curr ) {
return null;
} else {
return f( curr );
}
};
}
function streamViaArray( a ) {
var lastIndex = a.length - 1;
var d = -1;
return function() {
if ( lastIndex == d ) {
return null;
} else {
d += 1;
return a[ d ];
}
};
}
// # ~
function error( msg ) {
console.log( msg );
return false;
}
}
// ABOVE is the last line of the library. below is for the codepen demo
SkylabSortableListCore( $( '#list' ) );
// end codepen demo
Also see: Tab Triggers