0
Fork 0
mirror of https://github.com/fastmail/Squire.git synced 2024-12-22 07:13:08 -05:00

Add support mult-level lists.

* Hit tab to increase list depth, or call increaseListLevel method.
* Hit enter on a blank item to decrease list depth, or call decreaseListLevel method.
This commit is contained in:
Neil Jenkins 2014-04-07 13:05:44 +10:00
parent bee49bef40
commit 6b754d423c
5 changed files with 320 additions and 138 deletions

View file

@ -1,6 +1,6 @@
/* Copyright © 2011-2013 by Neil Jenkins. MIT Licensed. */
( function ( doc ) {
( function ( doc, undefined ) {
"use strict";
/*global doc, navigator */
@ -506,26 +506,40 @@ function mergeWithBlock ( block, next, range ) {
function mergeContainers ( node ) {
var prev = node.previousSibling,
first = node.firstChild;
first = node.firstChild,
isListItem = ( node.nodeName === 'LI' );
// Do not merge LIs, unless it only contains a UL
if ( isListItem && ( !first || !/^[OU]L$/.test( first.nodeName ) ) ) {
return;
}
if ( prev && areAlike( prev, node ) && isContainer( prev ) ) {
detach( node );
prev.appendChild( empty( node ) );
if ( first ) {
mergeContainers( first );
}
} else if ( isListItem ) {
prev = node.ownerDocument.createElement( 'div' );
node.insertBefore( prev, first );
fixCursor( prev );
}
}
function createElement ( doc, tag, props, children ) {
var el = doc.createElement( tag ),
attr, i, l;
attr, value, i, l;
if ( props instanceof Array ) {
children = props;
props = null;
}
if ( props ) {
for ( attr in props ) {
el.setAttribute( attr, props[ attr ] );
value = props[ attr ];
if ( value !== undefined ) {
el.setAttribute( attr, props[ attr ] );
}
}
}
if ( children ) {
@ -1076,6 +1090,7 @@ var expandRangeToBlockBoundaries = function ( range ) {
isInline,
isBlock,
isContainer,
getBlockWalker,
getPreviousBlock,
getNextBlock,
getNearest,
@ -2023,83 +2038,128 @@ var removeBlockQuote = function ( frag ) {
return frag;
};
var makeList = function ( self, nodes, type ) {
var i, l, node, tag, prev, replacement;
for ( i = 0, l = nodes.length; i < l; i += 1 ) {
node = nodes[i];
tag = node.nodeName;
if ( isBlock( node ) ) {
if ( tag !== 'LI' ) {
replacement = self.createElement( 'LI', {
'class': node.dir === 'rtl' ? 'dir-rtl' : '',
dir: node.dir
}, [
empty( node )
]);
if ( node.parentNode.nodeName === type ) {
replaceWith( node, replacement );
}
else if ( ( prev = node.previousSibling ) &&
prev.nodeName === type ) {
prev.appendChild( replacement );
detach( node );
i -= 1;
l -= 1;
}
else {
replaceWith(
node,
self.createElement( type, [
replacement
])
);
}
var makeList = function ( self, frag, type ) {
var walker = getBlockWalker( frag ),
node, tag, prev, newLi;
while ( node = walker.nextNode() ) {
tag = node.parentNode.nodeName;
if ( tag !== 'LI' ) {
newLi = self.createElement( 'LI', {
'class': node.dir === 'rtl' ? 'dir-rtl' : undefined,
dir: node.dir || undefined
});
// Have we replaced the previous block with a new <ul>/<ol>?
if ( ( prev = node.previousSibling ) &&
prev.nodeName === type ) {
prev.appendChild( newLi );
}
} else if ( isContainer( node ) ) {
if ( tag !== type && ( /^[DOU]L$/.test( tag ) ) ) {
// Otherwise, replace this block with the <ul>/<ol>
else {
replaceWith(
node,
self.createElement( type, [
newLi
])
);
}
newLi.appendChild( node );
} else {
node = node.parentNode.parentNode;
tag = node.nodeName;
if ( tag !== type && ( /^[OU]L$/.test( tag ) ) ) {
replaceWith( node,
self.createElement( type, [ empty( node ) ] )
);
} else {
makeList( self, node.childNodes, type );
}
}
}
};
var makeUnorderedList = function ( frag ) {
makeList( this, frag.childNodes, 'UL' );
makeList( this, frag, 'UL' );
return frag;
};
var makeOrderedList = function ( frag ) {
makeList( this, frag.childNodes, 'OL' );
makeList( this, frag, 'OL' );
return frag;
};
var removeList = function ( frag ) {
var lists = frag.querySelectorAll( 'UL, OL' ),
i, l, ll, list, listFrag, children, child;
for ( i = 0, l = lists.length; i < l; i += 1 ) {
list = lists[i];
listFrag = empty( list );
children = listFrag.childNodes;
ll = children.length;
while ( ll-- ) {
child = children[ll];
replaceWith( child, empty( child ) );
}
wrapTopLevelInline( listFrag, 'DIV' );
replaceWith( list, listFrag );
}
return frag;
};
var increaseListLevel = function ( frag ) {
var items = frag.querySelectorAll( 'LI' ),
i, l, item,
type, newParent;
for ( i = 0, l = items.length; i < l; i += 1 ) {
item = items[i];
if ( !isContainer( item.firstChild ) ) {
// type => 'UL' or 'OL'
type = item.parentNode.nodeName;
newParent = item.previousSibling;
if ( !newParent || !( newParent = newParent.lastChild ) ||
newParent.nodeName !== type ) {
replaceWith(
item,
this.createElement( 'LI', [
newParent = this.createElement( type )
])
);
}
newParent.appendChild( item );
}
}
return frag;
};
var decreaseListLevel = function ( frag ) {
var lists = frag.querySelectorAll( 'UL, OL' );
Array.prototype.filter.call( lists, function ( el ) {
return !getNearest( el.parentNode, 'UL' ) &&
!getNearest( el.parentNode, 'OL' );
}).forEach( function ( el ) {
var frag = empty( el ),
children = frag.childNodes,
l = children.length,
child;
while ( l-- ) {
child = children[l];
if ( child.nodeName === 'LI' ) {
frag.replaceChild( this.createElement( 'DIV', {
'class': child.dir === 'rtl' ? 'dir-rtl' : '',
dir: child.dir
}, [
empty( child )
]), child );
}
var items = frag.querySelectorAll( 'LI' );
Array.prototype.filter.call( items, function ( el ) {
return !isContainer( el.firstChild );
}).forEach( function ( item ) {
var parent = item.parentNode,
newParent = parent.parentNode,
first = item.firstChild,
node = first,
next;
if ( item.previousSibling ) {
parent = split( parent, item, newParent );
}
while ( node ) {
next = node.nextSibling;
if ( isContainer( node ) ) {
break;
}
newParent.insertBefore( node, parent );
node = next;
}
if ( newParent.nodeName === 'LI' && first.previousSibling ) {
split( newParent, first, newParent.parentNode );
}
while ( item !== frag && !item.childNodes.length ) {
parent = item.parentNode;
parent.removeChild( item );
item = parent;
}
replaceWith( el, frag );
}, this );
wrapTopLevelInline( frag, 'DIV' );
return frag;
};
@ -2689,7 +2749,8 @@ var keyHandlers = {
event.preventDefault();
// Must have some form of selection
var range = self.getSelection();
var range = self.getSelection(),
block, parent, tag, splitTag, nodeAfterSplit;
if ( !range ) { return; }
// Save undo checkpoint and add any links in the preceding section.
@ -2703,10 +2764,12 @@ var keyHandlers = {
deleteContentsOfRange( range );
}
var block = getStartBlockOfRange( range ),
tag = block ? block.nodeName : 'DIV',
splitTag = tagAfterSplit[ tag ],
nodeAfterSplit;
block = getStartBlockOfRange( range );
if ( block && ( parent = getNearest( block, 'LI' ) ) ) {
block = parent;
}
tag = block ? block.nodeName : 'DIV';
splitTag = tagAfterSplit[ tag ];
// If this is a malformed bit of document, just play it safe
// and insert a <br>.
@ -2725,7 +2788,7 @@ var keyHandlers = {
replacement;
if ( !splitTag ) {
// If the selection point is inside the block, we're going to
// rewrite it so our saved referece points won't be valid.
// rewrite it so our saved reference points won't be valid.
// Pick a node at a deeper point in the tree to avoid this.
if ( splitNode === block ) {
splitNode = splitOffset ?
@ -2943,6 +3006,31 @@ var keyHandlers = {
setTimeout( function () { afterDelete( self ); }, 0 );
}
},
tab: function ( self, event ) {
var range = self.getSelection(),
node, parent;
// If no selection and in an empty block
if ( range.collapsed &&
rangeDoesStartAtBlockBoundary( range ) &&
rangeDoesEndAtBlockBoundary( range ) ) {
node = getStartBlockOfRange( range );
// Iterate through the block's parents
while ( parent = node.parentNode ) {
// If we find a UL or OL (so are in a list, node must be an LI)
if ( /^[OU]L$/.test( parent.nodeName ) ) {
// AND the LI is not the first in the list
if ( node.previousSibling ) {
// Then increase the list level
event.preventDefault();
self.modifyBlocks( increaseListLevel, range );
}
break;
}
node = parent;
}
event.preventDefault();
}
},
space: function ( self ) {
var range = self.getSelection();
self._recordUndoState( range );
@ -3312,7 +3400,10 @@ proto.decreaseQuoteLevel = command( 'modifyBlocks', decreaseBlockQuoteLevel );
proto.makeUnorderedList = command( 'modifyBlocks', makeUnorderedList );
proto.makeOrderedList = command( 'modifyBlocks', makeOrderedList );
proto.removeList = command( 'modifyBlocks', decreaseListLevel );
proto.removeList = command( 'modifyBlocks', removeList );
proto.increaseListLevel = command( 'modifyBlocks', increaseListLevel );
proto.decreaseListLevel = command( 'modifyBlocks', decreaseListLevel );
/*global top, win, doc, Squire */
if ( top !== win ) {

File diff suppressed because one or more lines are too long

View file

@ -28,6 +28,7 @@
isInline,
isBlock,
isContainer,
getBlockWalker,
getPreviousBlock,
getNextBlock,
getNearest,
@ -975,83 +976,128 @@ var removeBlockQuote = function ( frag ) {
return frag;
};
var makeList = function ( self, nodes, type ) {
var i, l, node, tag, prev, replacement;
for ( i = 0, l = nodes.length; i < l; i += 1 ) {
node = nodes[i];
tag = node.nodeName;
if ( isBlock( node ) ) {
if ( tag !== 'LI' ) {
replacement = self.createElement( 'LI', {
'class': node.dir === 'rtl' ? 'dir-rtl' : '',
dir: node.dir
}, [
empty( node )
]);
if ( node.parentNode.nodeName === type ) {
replaceWith( node, replacement );
}
else if ( ( prev = node.previousSibling ) &&
prev.nodeName === type ) {
prev.appendChild( replacement );
detach( node );
i -= 1;
l -= 1;
}
else {
replaceWith(
node,
self.createElement( type, [
replacement
])
);
}
var makeList = function ( self, frag, type ) {
var walker = getBlockWalker( frag ),
node, tag, prev, newLi;
while ( node = walker.nextNode() ) {
tag = node.parentNode.nodeName;
if ( tag !== 'LI' ) {
newLi = self.createElement( 'LI', {
'class': node.dir === 'rtl' ? 'dir-rtl' : undefined,
dir: node.dir || undefined
});
// Have we replaced the previous block with a new <ul>/<ol>?
if ( ( prev = node.previousSibling ) &&
prev.nodeName === type ) {
prev.appendChild( newLi );
}
} else if ( isContainer( node ) ) {
if ( tag !== type && ( /^[DOU]L$/.test( tag ) ) ) {
// Otherwise, replace this block with the <ul>/<ol>
else {
replaceWith(
node,
self.createElement( type, [
newLi
])
);
}
newLi.appendChild( node );
} else {
node = node.parentNode.parentNode;
tag = node.nodeName;
if ( tag !== type && ( /^[OU]L$/.test( tag ) ) ) {
replaceWith( node,
self.createElement( type, [ empty( node ) ] )
);
} else {
makeList( self, node.childNodes, type );
}
}
}
};
var makeUnorderedList = function ( frag ) {
makeList( this, frag.childNodes, 'UL' );
makeList( this, frag, 'UL' );
return frag;
};
var makeOrderedList = function ( frag ) {
makeList( this, frag.childNodes, 'OL' );
makeList( this, frag, 'OL' );
return frag;
};
var removeList = function ( frag ) {
var lists = frag.querySelectorAll( 'UL, OL' ),
i, l, ll, list, listFrag, children, child;
for ( i = 0, l = lists.length; i < l; i += 1 ) {
list = lists[i];
listFrag = empty( list );
children = listFrag.childNodes;
ll = children.length;
while ( ll-- ) {
child = children[ll];
replaceWith( child, empty( child ) );
}
wrapTopLevelInline( listFrag, 'DIV' );
replaceWith( list, listFrag );
}
return frag;
};
var increaseListLevel = function ( frag ) {
var items = frag.querySelectorAll( 'LI' ),
i, l, item,
type, newParent;
for ( i = 0, l = items.length; i < l; i += 1 ) {
item = items[i];
if ( !isContainer( item.firstChild ) ) {
// type => 'UL' or 'OL'
type = item.parentNode.nodeName;
newParent = item.previousSibling;
if ( !newParent || !( newParent = newParent.lastChild ) ||
newParent.nodeName !== type ) {
replaceWith(
item,
this.createElement( 'LI', [
newParent = this.createElement( type )
])
);
}
newParent.appendChild( item );
}
}
return frag;
};
var decreaseListLevel = function ( frag ) {
var lists = frag.querySelectorAll( 'UL, OL' );
Array.prototype.filter.call( lists, function ( el ) {
return !getNearest( el.parentNode, 'UL' ) &&
!getNearest( el.parentNode, 'OL' );
}).forEach( function ( el ) {
var frag = empty( el ),
children = frag.childNodes,
l = children.length,
child;
while ( l-- ) {
child = children[l];
if ( child.nodeName === 'LI' ) {
frag.replaceChild( this.createElement( 'DIV', {
'class': child.dir === 'rtl' ? 'dir-rtl' : '',
dir: child.dir
}, [
empty( child )
]), child );
}
var items = frag.querySelectorAll( 'LI' );
Array.prototype.filter.call( items, function ( el ) {
return !isContainer( el.firstChild );
}).forEach( function ( item ) {
var parent = item.parentNode,
newParent = parent.parentNode,
first = item.firstChild,
node = first,
next;
if ( item.previousSibling ) {
parent = split( parent, item, newParent );
}
while ( node ) {
next = node.nextSibling;
if ( isContainer( node ) ) {
break;
}
newParent.insertBefore( node, parent );
node = next;
}
if ( newParent.nodeName === 'LI' && first.previousSibling ) {
split( newParent, first, newParent.parentNode );
}
while ( item !== frag && !item.childNodes.length ) {
parent = item.parentNode;
parent.removeChild( item );
item = parent;
}
replaceWith( el, frag );
}, this );
wrapTopLevelInline( frag, 'DIV' );
return frag;
};
@ -1641,7 +1687,8 @@ var keyHandlers = {
event.preventDefault();
// Must have some form of selection
var range = self.getSelection();
var range = self.getSelection(),
block, parent, tag, splitTag, nodeAfterSplit;
if ( !range ) { return; }
// Save undo checkpoint and add any links in the preceding section.
@ -1655,10 +1702,12 @@ var keyHandlers = {
deleteContentsOfRange( range );
}
var block = getStartBlockOfRange( range ),
tag = block ? block.nodeName : 'DIV',
splitTag = tagAfterSplit[ tag ],
nodeAfterSplit;
block = getStartBlockOfRange( range );
if ( block && ( parent = getNearest( block, 'LI' ) ) ) {
block = parent;
}
tag = block ? block.nodeName : 'DIV';
splitTag = tagAfterSplit[ tag ];
// If this is a malformed bit of document, just play it safe
// and insert a <br>.
@ -1677,7 +1726,7 @@ var keyHandlers = {
replacement;
if ( !splitTag ) {
// If the selection point is inside the block, we're going to
// rewrite it so our saved referece points won't be valid.
// rewrite it so our saved reference points won't be valid.
// Pick a node at a deeper point in the tree to avoid this.
if ( splitNode === block ) {
splitNode = splitOffset ?
@ -1895,6 +1944,31 @@ var keyHandlers = {
setTimeout( function () { afterDelete( self ); }, 0 );
}
},
tab: function ( self, event ) {
var range = self.getSelection(),
node, parent;
// If no selection and in an empty block
if ( range.collapsed &&
rangeDoesStartAtBlockBoundary( range ) &&
rangeDoesEndAtBlockBoundary( range ) ) {
node = getStartBlockOfRange( range );
// Iterate through the block's parents
while ( parent = node.parentNode ) {
// If we find a UL or OL (so are in a list, node must be an LI)
if ( parent.nodeName === 'UL' || parent.nodeName === 'OL' ) {
// AND the LI is not the first in the list
if ( node.previousSibling ) {
// Then increase the list level
event.preventDefault();
self.modifyBlocks( increaseListLevel, range );
}
break;
}
node = parent;
}
event.preventDefault();
}
},
space: function ( self ) {
var range = self.getSelection();
self._recordUndoState( range );
@ -2264,4 +2338,7 @@ proto.decreaseQuoteLevel = command( 'modifyBlocks', decreaseBlockQuoteLevel );
proto.makeUnorderedList = command( 'modifyBlocks', makeUnorderedList );
proto.makeOrderedList = command( 'modifyBlocks', makeOrderedList );
proto.removeList = command( 'modifyBlocks', decreaseListLevel );
proto.removeList = command( 'modifyBlocks', removeList );
proto.increaseListLevel = command( 'modifyBlocks', increaseListLevel );
proto.decreaseListLevel = command( 'modifyBlocks', decreaseListLevel );

View file

@ -364,26 +364,40 @@ function mergeWithBlock ( block, next, range ) {
function mergeContainers ( node ) {
var prev = node.previousSibling,
first = node.firstChild;
first = node.firstChild,
isListItem = ( node.nodeName === 'LI' );
// Do not merge LIs, unless it only contains a UL
if ( isListItem && ( !first || !/^[OU]L$/.test( first.nodeName ) ) ) {
return;
}
if ( prev && areAlike( prev, node ) && isContainer( prev ) ) {
detach( node );
prev.appendChild( empty( node ) );
if ( first ) {
mergeContainers( first );
}
} else if ( isListItem ) {
prev = node.ownerDocument.createElement( 'div' );
node.insertBefore( prev, first );
fixCursor( prev );
}
}
function createElement ( doc, tag, props, children ) {
var el = doc.createElement( tag ),
attr, i, l;
attr, value, i, l;
if ( props instanceof Array ) {
children = props;
props = null;
}
if ( props ) {
for ( attr in props ) {
el.setAttribute( attr, props[ attr ] );
value = props[ attr ];
if ( value !== undefined ) {
el.setAttribute( attr, props[ attr ] );
}
}
}
if ( children ) {

View file

@ -1,5 +1,5 @@
/* Copyright © 2011-2013 by Neil Jenkins. MIT Licensed. */
( function ( doc ) {
( function ( doc, undefined ) {
"use strict";