0
Fork 0
mirror of https://github.com/fastmail/Squire.git synced 2024-12-31 11:54:03 -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. */ /* Copyright © 2011-2013 by Neil Jenkins. MIT Licensed. */
( function ( doc ) { ( function ( doc, undefined ) {
"use strict"; "use strict";
/*global doc, navigator */ /*global doc, navigator */
@ -506,26 +506,40 @@ function mergeWithBlock ( block, next, range ) {
function mergeContainers ( node ) { function mergeContainers ( node ) {
var prev = node.previousSibling, 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 ) ) { if ( prev && areAlike( prev, node ) && isContainer( prev ) ) {
detach( node ); detach( node );
prev.appendChild( empty( node ) ); prev.appendChild( empty( node ) );
if ( first ) { if ( first ) {
mergeContainers( first ); mergeContainers( first );
} }
} else if ( isListItem ) {
prev = node.ownerDocument.createElement( 'div' );
node.insertBefore( prev, first );
fixCursor( prev );
} }
} }
function createElement ( doc, tag, props, children ) { function createElement ( doc, tag, props, children ) {
var el = doc.createElement( tag ), var el = doc.createElement( tag ),
attr, i, l; attr, value, i, l;
if ( props instanceof Array ) { if ( props instanceof Array ) {
children = props; children = props;
props = null; props = null;
} }
if ( props ) { if ( props ) {
for ( attr in props ) { for ( attr in props ) {
el.setAttribute( attr, props[ attr ] ); value = props[ attr ];
if ( value !== undefined ) {
el.setAttribute( attr, props[ attr ] );
}
} }
} }
if ( children ) { if ( children ) {
@ -1076,6 +1090,7 @@ var expandRangeToBlockBoundaries = function ( range ) {
isInline, isInline,
isBlock, isBlock,
isContainer, isContainer,
getBlockWalker,
getPreviousBlock, getPreviousBlock,
getNextBlock, getNextBlock,
getNearest, getNearest,
@ -2023,83 +2038,128 @@ var removeBlockQuote = function ( frag ) {
return frag; return frag;
}; };
var makeList = function ( self, nodes, type ) { var makeList = function ( self, frag, type ) {
var i, l, node, tag, prev, replacement; var walker = getBlockWalker( frag ),
for ( i = 0, l = nodes.length; i < l; i += 1 ) { node, tag, prev, newLi;
node = nodes[i];
tag = node.nodeName; while ( node = walker.nextNode() ) {
if ( isBlock( node ) ) { tag = node.parentNode.nodeName;
if ( tag !== 'LI' ) { if ( tag !== 'LI' ) {
replacement = self.createElement( 'LI', { newLi = self.createElement( 'LI', {
'class': node.dir === 'rtl' ? 'dir-rtl' : '', 'class': node.dir === 'rtl' ? 'dir-rtl' : undefined,
dir: node.dir dir: node.dir || undefined
}, [ });
empty( node ) // Have we replaced the previous block with a new <ul>/<ol>?
]); if ( ( prev = node.previousSibling ) &&
if ( node.parentNode.nodeName === type ) { prev.nodeName === type ) {
replaceWith( node, replacement ); prev.appendChild( newLi );
}
else if ( ( prev = node.previousSibling ) &&
prev.nodeName === type ) {
prev.appendChild( replacement );
detach( node );
i -= 1;
l -= 1;
}
else {
replaceWith(
node,
self.createElement( type, [
replacement
])
);
}
} }
} else if ( isContainer( node ) ) { // Otherwise, replace this block with the <ul>/<ol>
if ( tag !== type && ( /^[DOU]L$/.test( tag ) ) ) { 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, replaceWith( node,
self.createElement( type, [ empty( node ) ] ) self.createElement( type, [ empty( node ) ] )
); );
} else {
makeList( self, node.childNodes, type );
} }
} }
} }
}; };
var makeUnorderedList = function ( frag ) { var makeUnorderedList = function ( frag ) {
makeList( this, frag.childNodes, 'UL' ); makeList( this, frag, 'UL' );
return frag; return frag;
}; };
var makeOrderedList = function ( 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; return frag;
}; };
var decreaseListLevel = function ( frag ) { var decreaseListLevel = function ( frag ) {
var lists = frag.querySelectorAll( 'UL, OL' ); var items = frag.querySelectorAll( 'LI' );
Array.prototype.filter.call( lists, function ( el ) { Array.prototype.filter.call( items, function ( el ) {
return !getNearest( el.parentNode, 'UL' ) && return !isContainer( el.firstChild );
!getNearest( el.parentNode, 'OL' ); }).forEach( function ( item ) {
}).forEach( function ( el ) { var parent = item.parentNode,
var frag = empty( el ), newParent = parent.parentNode,
children = frag.childNodes, first = item.firstChild,
l = children.length, node = first,
child; next;
while ( l-- ) { if ( item.previousSibling ) {
child = children[l]; parent = split( parent, item, newParent );
if ( child.nodeName === 'LI' ) { }
frag.replaceChild( this.createElement( 'DIV', { while ( node ) {
'class': child.dir === 'rtl' ? 'dir-rtl' : '', next = node.nextSibling;
dir: child.dir if ( isContainer( node ) ) {
}, [ break;
empty( child ) }
]), child ); 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 ); }, this );
wrapTopLevelInline( frag, 'DIV' );
return frag; return frag;
}; };
@ -2689,7 +2749,8 @@ var keyHandlers = {
event.preventDefault(); event.preventDefault();
// Must have some form of selection // Must have some form of selection
var range = self.getSelection(); var range = self.getSelection(),
block, parent, tag, splitTag, nodeAfterSplit;
if ( !range ) { return; } if ( !range ) { return; }
// Save undo checkpoint and add any links in the preceding section. // Save undo checkpoint and add any links in the preceding section.
@ -2703,10 +2764,12 @@ var keyHandlers = {
deleteContentsOfRange( range ); deleteContentsOfRange( range );
} }
var block = getStartBlockOfRange( range ), block = getStartBlockOfRange( range );
tag = block ? block.nodeName : 'DIV', if ( block && ( parent = getNearest( block, 'LI' ) ) ) {
splitTag = tagAfterSplit[ tag ], block = parent;
nodeAfterSplit; }
tag = block ? block.nodeName : 'DIV';
splitTag = tagAfterSplit[ tag ];
// If this is a malformed bit of document, just play it safe // If this is a malformed bit of document, just play it safe
// and insert a <br>. // and insert a <br>.
@ -2725,7 +2788,7 @@ var keyHandlers = {
replacement; replacement;
if ( !splitTag ) { if ( !splitTag ) {
// If the selection point is inside the block, we're going to // 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. // Pick a node at a deeper point in the tree to avoid this.
if ( splitNode === block ) { if ( splitNode === block ) {
splitNode = splitOffset ? splitNode = splitOffset ?
@ -2943,6 +3006,31 @@ var keyHandlers = {
setTimeout( function () { afterDelete( self ); }, 0 ); 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 ) { space: function ( self ) {
var range = self.getSelection(); var range = self.getSelection();
self._recordUndoState( range ); self._recordUndoState( range );
@ -3312,7 +3400,10 @@ proto.decreaseQuoteLevel = command( 'modifyBlocks', decreaseBlockQuoteLevel );
proto.makeUnorderedList = command( 'modifyBlocks', makeUnorderedList ); proto.makeUnorderedList = command( 'modifyBlocks', makeUnorderedList );
proto.makeOrderedList = command( 'modifyBlocks', makeOrderedList ); 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 */ /*global top, win, doc, Squire */
if ( top !== win ) { if ( top !== win ) {

File diff suppressed because one or more lines are too long

View file

@ -28,6 +28,7 @@
isInline, isInline,
isBlock, isBlock,
isContainer, isContainer,
getBlockWalker,
getPreviousBlock, getPreviousBlock,
getNextBlock, getNextBlock,
getNearest, getNearest,
@ -975,83 +976,128 @@ var removeBlockQuote = function ( frag ) {
return frag; return frag;
}; };
var makeList = function ( self, nodes, type ) { var makeList = function ( self, frag, type ) {
var i, l, node, tag, prev, replacement; var walker = getBlockWalker( frag ),
for ( i = 0, l = nodes.length; i < l; i += 1 ) { node, tag, prev, newLi;
node = nodes[i];
tag = node.nodeName; while ( node = walker.nextNode() ) {
if ( isBlock( node ) ) { tag = node.parentNode.nodeName;
if ( tag !== 'LI' ) { if ( tag !== 'LI' ) {
replacement = self.createElement( 'LI', { newLi = self.createElement( 'LI', {
'class': node.dir === 'rtl' ? 'dir-rtl' : '', 'class': node.dir === 'rtl' ? 'dir-rtl' : undefined,
dir: node.dir dir: node.dir || undefined
}, [ });
empty( node ) // Have we replaced the previous block with a new <ul>/<ol>?
]); if ( ( prev = node.previousSibling ) &&
if ( node.parentNode.nodeName === type ) { prev.nodeName === type ) {
replaceWith( node, replacement ); prev.appendChild( newLi );
}
else if ( ( prev = node.previousSibling ) &&
prev.nodeName === type ) {
prev.appendChild( replacement );
detach( node );
i -= 1;
l -= 1;
}
else {
replaceWith(
node,
self.createElement( type, [
replacement
])
);
}
} }
} else if ( isContainer( node ) ) { // Otherwise, replace this block with the <ul>/<ol>
if ( tag !== type && ( /^[DOU]L$/.test( tag ) ) ) { 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, replaceWith( node,
self.createElement( type, [ empty( node ) ] ) self.createElement( type, [ empty( node ) ] )
); );
} else {
makeList( self, node.childNodes, type );
} }
} }
} }
}; };
var makeUnorderedList = function ( frag ) { var makeUnorderedList = function ( frag ) {
makeList( this, frag.childNodes, 'UL' ); makeList( this, frag, 'UL' );
return frag; return frag;
}; };
var makeOrderedList = function ( 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; return frag;
}; };
var decreaseListLevel = function ( frag ) { var decreaseListLevel = function ( frag ) {
var lists = frag.querySelectorAll( 'UL, OL' ); var items = frag.querySelectorAll( 'LI' );
Array.prototype.filter.call( lists, function ( el ) { Array.prototype.filter.call( items, function ( el ) {
return !getNearest( el.parentNode, 'UL' ) && return !isContainer( el.firstChild );
!getNearest( el.parentNode, 'OL' ); }).forEach( function ( item ) {
}).forEach( function ( el ) { var parent = item.parentNode,
var frag = empty( el ), newParent = parent.parentNode,
children = frag.childNodes, first = item.firstChild,
l = children.length, node = first,
child; next;
while ( l-- ) { if ( item.previousSibling ) {
child = children[l]; parent = split( parent, item, newParent );
if ( child.nodeName === 'LI' ) { }
frag.replaceChild( this.createElement( 'DIV', { while ( node ) {
'class': child.dir === 'rtl' ? 'dir-rtl' : '', next = node.nextSibling;
dir: child.dir if ( isContainer( node ) ) {
}, [ break;
empty( child ) }
]), child ); 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 ); }, this );
wrapTopLevelInline( frag, 'DIV' );
return frag; return frag;
}; };
@ -1641,7 +1687,8 @@ var keyHandlers = {
event.preventDefault(); event.preventDefault();
// Must have some form of selection // Must have some form of selection
var range = self.getSelection(); var range = self.getSelection(),
block, parent, tag, splitTag, nodeAfterSplit;
if ( !range ) { return; } if ( !range ) { return; }
// Save undo checkpoint and add any links in the preceding section. // Save undo checkpoint and add any links in the preceding section.
@ -1655,10 +1702,12 @@ var keyHandlers = {
deleteContentsOfRange( range ); deleteContentsOfRange( range );
} }
var block = getStartBlockOfRange( range ), block = getStartBlockOfRange( range );
tag = block ? block.nodeName : 'DIV', if ( block && ( parent = getNearest( block, 'LI' ) ) ) {
splitTag = tagAfterSplit[ tag ], block = parent;
nodeAfterSplit; }
tag = block ? block.nodeName : 'DIV';
splitTag = tagAfterSplit[ tag ];
// If this is a malformed bit of document, just play it safe // If this is a malformed bit of document, just play it safe
// and insert a <br>. // and insert a <br>.
@ -1677,7 +1726,7 @@ var keyHandlers = {
replacement; replacement;
if ( !splitTag ) { if ( !splitTag ) {
// If the selection point is inside the block, we're going to // 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. // Pick a node at a deeper point in the tree to avoid this.
if ( splitNode === block ) { if ( splitNode === block ) {
splitNode = splitOffset ? splitNode = splitOffset ?
@ -1895,6 +1944,31 @@ var keyHandlers = {
setTimeout( function () { afterDelete( self ); }, 0 ); 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 ) { space: function ( self ) {
var range = self.getSelection(); var range = self.getSelection();
self._recordUndoState( range ); self._recordUndoState( range );
@ -2264,4 +2338,7 @@ proto.decreaseQuoteLevel = command( 'modifyBlocks', decreaseBlockQuoteLevel );
proto.makeUnorderedList = command( 'modifyBlocks', makeUnorderedList ); proto.makeUnorderedList = command( 'modifyBlocks', makeUnorderedList );
proto.makeOrderedList = command( 'modifyBlocks', makeOrderedList ); 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 ) { function mergeContainers ( node ) {
var prev = node.previousSibling, 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 ) ) { if ( prev && areAlike( prev, node ) && isContainer( prev ) ) {
detach( node ); detach( node );
prev.appendChild( empty( node ) ); prev.appendChild( empty( node ) );
if ( first ) { if ( first ) {
mergeContainers( first ); mergeContainers( first );
} }
} else if ( isListItem ) {
prev = node.ownerDocument.createElement( 'div' );
node.insertBefore( prev, first );
fixCursor( prev );
} }
} }
function createElement ( doc, tag, props, children ) { function createElement ( doc, tag, props, children ) {
var el = doc.createElement( tag ), var el = doc.createElement( tag ),
attr, i, l; attr, value, i, l;
if ( props instanceof Array ) { if ( props instanceof Array ) {
children = props; children = props;
props = null; props = null;
} }
if ( props ) { if ( props ) {
for ( attr in props ) { for ( attr in props ) {
el.setAttribute( attr, props[ attr ] ); value = props[ attr ];
if ( value !== undefined ) {
el.setAttribute( attr, props[ attr ] );
}
} }
} }
if ( children ) { if ( children ) {

View file

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