squire-rails 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 49a7361e5ff8621dc290906bbd13199217b5f69d
4
- data.tar.gz: 4a4ea25f90b9b510a290d35db35bea378b163466
3
+ metadata.gz: 604f8843d5e2ecfd3a4c5975af0c37e74d79b3a9
4
+ data.tar.gz: a1d403eccb978254d6df20ea2a6b08a6fd6893b8
5
5
  SHA512:
6
- metadata.gz: e0d6494032e4bd0e63885cf44c990f17b1346ca2eb38f7b64cccff2705b0c05fb6c8972a00919c4d37a1ac6aebd6c3a6cdd57ba32e7c61d79a95bd36caf1c7ba
7
- data.tar.gz: 51b2d771a1a970fbb2bf621be39f9a9adfdc07bd932644a69d5b1434c3b5c797068a7006096b3eb810ade97ab35255f5f59e60cfc670d389ad09de19b80a2ed9
6
+ metadata.gz: a72947f12e20854bc725199576c0d666814447a4ea656bf65774f4195b8d2211e2ef509eb7acb1f2725932a2449509203ea69d69004763c9fed702a99dd65757
7
+ data.tar.gz: bf05167d9a969b2a9de03d4faefbc82d7cdd189f093ba4a9cc9c34f5fb4d6efa58931fe3edf123dd0b173bf7f679b7ddc91d3a19906340298e2a32ee6ce5d51c
@@ -1,3 +1,3 @@
1
1
  module SquireRails
2
- VERSION = "0.0.8"
2
+ VERSION = "0.0.9"
3
3
  end
@@ -7,6 +7,7 @@
7
7
  var DOCUMENT_POSITION_PRECEDING = 2; // Node.DOCUMENT_POSITION_PRECEDING
8
8
  var ELEMENT_NODE = 1; // Node.ELEMENT_NODE;
9
9
  var TEXT_NODE = 3; // Node.TEXT_NODE;
10
+ var DOCUMENT_FRAGMENT_NODE = 11; // Node.DOCUMENT_FRAGMENT_NODE;
10
11
  var SHOW_ELEMENT = 1; // NodeFilter.SHOW_ELEMENT;
11
12
  var SHOW_TEXT = 4; // NodeFilter.SHOW_TEXT;
12
13
 
@@ -140,7 +141,35 @@ TreeWalker.prototype.previousNode = function () {
140
141
  }
141
142
  };
142
143
 
143
- var inlineNodeNames = /^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:ATA|FN|EL)|EM|FONT|HR|I(?:NPUT|MG|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:U[BP]|PAN|TR(?:IKE|ONG)|MALL|AMP)?|U|VAR|WBR)$/;
144
+ // Previous node in post-order.
145
+ TreeWalker.prototype.previousPONode = function () {
146
+ var current = this.currentNode,
147
+ root = this.root,
148
+ nodeType = this.nodeType,
149
+ filter = this.filter,
150
+ node;
151
+ while ( true ) {
152
+ node = current.lastChild;
153
+ while ( !node && current ) {
154
+ if ( current === root ) {
155
+ break;
156
+ }
157
+ node = current.previousSibling;
158
+ if ( !node ) { current = current.parentNode; }
159
+ }
160
+ if ( !node ) {
161
+ return null;
162
+ }
163
+ if ( ( typeToBitArray[ node.nodeType ] & nodeType ) &&
164
+ filter( node ) ) {
165
+ this.currentNode = node;
166
+ return node;
167
+ }
168
+ current = node;
169
+ }
170
+ };
171
+
172
+ var inlineNodeNames = /^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:ATA|EL|FN)|EM|FONT|HR|I(?:MG|NPUT|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:AMP|MALL|PAN|TR(?:IKE|ONG)|U[BP])?|U|VAR|WBR)$/;
144
173
 
145
174
  var leafNodeNames = {
146
175
  BR: 1,
@@ -172,7 +201,7 @@ function hasTagAttributes ( node, tag, attributes ) {
172
201
  return true;
173
202
  }
174
203
  function areAlike ( node, node2 ) {
175
- return (
204
+ return !isLeaf( node ) && (
176
205
  node.nodeType === node2.nodeType &&
177
206
  node.nodeName === node2.nodeName &&
178
207
  node.className === node2.className &&
@@ -189,11 +218,13 @@ function isInline ( node ) {
189
218
  return inlineNodeNames.test( node.nodeName );
190
219
  }
191
220
  function isBlock ( node ) {
192
- return node.nodeType === ELEMENT_NODE &&
221
+ var type = node.nodeType;
222
+ return ( type === ELEMENT_NODE || type === DOCUMENT_FRAGMENT_NODE ) &&
193
223
  !isInline( node ) && every( node.childNodes, isInline );
194
224
  }
195
225
  function isContainer ( node ) {
196
- return node.nodeType === ELEMENT_NODE &&
226
+ var type = node.nodeType;
227
+ return ( type === ELEMENT_NODE || type === DOCUMENT_FRAGMENT_NODE ) &&
197
228
  !isInline( node ) && !isBlock( node );
198
229
  }
199
230
 
@@ -667,7 +698,7 @@ var insertNodeInRange = function ( range, node ) {
667
698
 
668
699
  childCount = children.length;
669
700
 
670
- if ( startOffset === childCount) {
701
+ if ( startOffset === childCount ) {
671
702
  startContainer.appendChild( node );
672
703
  } else {
673
704
  startContainer.insertBefore( node, children[ startOffset ] );
@@ -735,8 +766,16 @@ var extractContentsOfRange = function ( range, common ) {
735
766
 
736
767
  var deleteContentsOfRange = function ( range ) {
737
768
  // Move boundaries up as much as possible to reduce need to split.
769
+ // But we need to check whether we've moved the boundary outside of a
770
+ // block. If so, the entire block will be removed, so we shouldn't merge
771
+ // later.
738
772
  moveRangeBoundariesUpTree( range );
739
773
 
774
+ var startBlock = range.startContainer,
775
+ endBlock = range.endContainer,
776
+ needsMerge = ( isInline( startBlock ) || isBlock( startBlock ) ) &&
777
+ ( isInline( endBlock ) || isBlock( endBlock ) );
778
+
740
779
  // Remove selected range
741
780
  extractContentsOfRange( range );
742
781
 
@@ -746,10 +785,12 @@ var deleteContentsOfRange = function ( range ) {
746
785
  moveRangeBoundariesDownTree( range );
747
786
 
748
787
  // If we split into two different blocks, merge the blocks.
749
- var startBlock = getStartBlockOfRange( range ),
788
+ if ( needsMerge ) {
789
+ startBlock = getStartBlockOfRange( range );
750
790
  endBlock = getEndBlockOfRange( range );
751
- if ( startBlock && endBlock && startBlock !== endBlock ) {
752
- mergeWithBlock( startBlock, endBlock, range );
791
+ if ( startBlock && endBlock && startBlock !== endBlock ) {
792
+ mergeWithBlock( startBlock, endBlock, range );
793
+ }
753
794
  }
754
795
 
755
796
  // Ensure block has necessary children
@@ -763,6 +804,8 @@ var deleteContentsOfRange = function ( range ) {
763
804
  if ( !child || child.nodeName === 'BR' ) {
764
805
  fixCursor( body );
765
806
  range.selectNodeContents( body.firstChild );
807
+ } else {
808
+ range.collapse( false );
766
809
  }
767
810
  };
768
811
 
@@ -788,15 +831,13 @@ var insertTreeFragmentIntoRange = function ( range, frag ) {
788
831
  // Move range down into text nodes
789
832
  moveRangeBoundariesDownTree( range );
790
833
 
791
- // If inline, just insert at the current position.
792
834
  if ( allInline ) {
835
+ // If inline, just insert at the current position.
793
836
  insertNodeInRange( range, frag );
794
837
  range.collapse( false );
795
- }
796
- // Otherwise, split up to blockquote (if a parent) or body, insert inline
797
- // before and after split and insert block in between split, then merge
798
- // containers.
799
- else {
838
+ } else {
839
+ // Otherwise...
840
+ // 1. Split up to blockquote (if a parent) or body
800
841
  var splitPoint = range.startContainer,
801
842
  nodeAfterSplit = split( splitPoint, range.startOffset,
802
843
  getNearest( splitPoint.parentNode, 'BLOCKQUOTE' ) ||
@@ -807,11 +848,16 @@ var insertTreeFragmentIntoRange = function ( range, frag ) {
807
848
  endContainer = nodeAfterSplit,
808
849
  endOffset = 0,
809
850
  parent = nodeAfterSplit.parentNode,
810
- child, node;
851
+ child, node, prev, next, startAnchor;
811
852
 
853
+ // 2. Move down into edge either side of split and insert any inline
854
+ // nodes at the beginning/end of the fragment
812
855
  while ( ( child = startContainer.lastChild ) &&
813
- child.nodeType === ELEMENT_NODE &&
814
- child.nodeName !== 'BR' ) {
856
+ child.nodeType === ELEMENT_NODE ) {
857
+ if ( child.nodeName === 'BR' ) {
858
+ startOffset -= 1;
859
+ break;
860
+ }
815
861
  startContainer = child;
816
862
  startOffset = startContainer.childNodes.length;
817
863
  }
@@ -820,40 +866,68 @@ var insertTreeFragmentIntoRange = function ( range, frag ) {
820
866
  child.nodeName !== 'BR' ) {
821
867
  endContainer = child;
822
868
  }
869
+ startAnchor = startContainer.childNodes[ startOffset ] || null;
823
870
  while ( ( child = frag.firstChild ) && isInline( child ) ) {
824
- startContainer.appendChild( child );
871
+ startContainer.insertBefore( child, startAnchor );
825
872
  }
826
873
  while ( ( child = frag.lastChild ) && isInline( child ) ) {
827
874
  endContainer.insertBefore( child, endContainer.firstChild );
828
875
  endOffset += 1;
829
876
  }
830
877
 
831
- // Fix cursor then insert block(s)
878
+ // 3. Fix cursor then insert block(s) in the fragment
832
879
  node = frag;
833
880
  while ( node = getNextBlock( node ) ) {
834
881
  fixCursor( node );
835
882
  }
836
883
  parent.insertBefore( frag, nodeAfterSplit );
837
884
 
838
- // Remove empty nodes created by split and merge inserted containers
839
- // with edges of split
840
- node = nodeAfterSplit.previousSibling;
841
- if ( !nodeAfterSplit.textContent ) {
842
- parent.removeChild( nodeAfterSplit );
843
- } else {
844
- mergeContainers( nodeAfterSplit );
885
+ // 4. Remove empty nodes created either side of split, then
886
+ // merge containers at the edges.
887
+ next = nodeBeforeSplit.nextSibling;
888
+ node = getPreviousBlock( next );
889
+ if ( !/\S/.test( node.textContent ) ) {
890
+ do {
891
+ parent = node.parentNode;
892
+ parent.removeChild( node );
893
+ node = parent;
894
+ } while ( parent && !parent.lastChild &&
895
+ parent.nodeName !== 'BODY' );
845
896
  }
846
- if ( !nodeAfterSplit.parentNode ) {
847
- endContainer = node;
848
- endOffset = getLength( endContainer );
897
+ if ( !nodeBeforeSplit.parentNode ) {
898
+ nodeBeforeSplit = next.previousSibling;
899
+ }
900
+ if ( !startContainer.parentNode ) {
901
+ startContainer = nodeBeforeSplit || next.parentNode;
902
+ startOffset = nodeBeforeSplit ?
903
+ nodeBeforeSplit.childNodes.length : 0;
904
+ }
905
+ // Merge inserted containers with edges of split
906
+ if ( isContainer( next ) ) {
907
+ mergeContainers( next );
849
908
  }
850
909
 
851
- if ( !nodeBeforeSplit.textContent) {
852
- startContainer = nodeBeforeSplit.nextSibling;
853
- startOffset = 0;
854
- parent.removeChild( nodeBeforeSplit );
855
- } else {
856
- mergeContainers( nodeBeforeSplit );
910
+ prev = nodeAfterSplit.previousSibling;
911
+ node = isBlock( nodeAfterSplit ) ?
912
+ nodeAfterSplit : getNextBlock( nodeAfterSplit );
913
+ if ( !/\S/.test( node.textContent ) ) {
914
+ do {
915
+ parent = node.parentNode;
916
+ parent.removeChild( node );
917
+ node = parent;
918
+ } while ( parent && !parent.lastChild &&
919
+ parent.nodeName !== 'BODY' );
920
+ }
921
+ if ( !nodeAfterSplit.parentNode ) {
922
+ nodeAfterSplit = prev.nextSibling;
923
+ }
924
+ if ( !endOffset ) {
925
+ endContainer = prev;
926
+ endOffset = prev.childNodes.length;
927
+ }
928
+ // Merge inserted containers with edges of split
929
+ if ( nodeAfterSplit && isContainer( nodeAfterSplit ) ) {
930
+ mergeContainers( nodeAfterSplit );
857
931
  }
858
932
 
859
933
  range.setStart( startContainer, startOffset );
@@ -1022,6 +1096,7 @@ var rangeDoesStartAtBlockBoundary = function ( range ) {
1022
1096
  startOffset = range.startOffset;
1023
1097
 
1024
1098
  // If in the middle or end of a text node, we're not at the boundary.
1099
+ contentWalker.root = null;
1025
1100
  if ( startContainer.nodeType === TEXT_NODE ) {
1026
1101
  if ( startOffset ) {
1027
1102
  return false;
@@ -1044,6 +1119,7 @@ var rangeDoesEndAtBlockBoundary = function ( range ) {
1044
1119
 
1045
1120
  // If in a text node with content, and not at the end, we're not
1046
1121
  // at the boundary
1122
+ contentWalker.root = null;
1047
1123
  if ( endContainer.nodeType === TEXT_NODE ) {
1048
1124
  length = endContainer.data.length;
1049
1125
  if ( length && endOffset < length ) {
@@ -1073,6 +1149,77 @@ var expandRangeToBlockBoundaries = function ( range ) {
1073
1149
  }
1074
1150
  };
1075
1151
 
1152
+ var keys = {
1153
+ 8: 'backspace',
1154
+ 9: 'tab',
1155
+ 13: 'enter',
1156
+ 32: 'space',
1157
+ 33: 'pageup',
1158
+ 34: 'pagedown',
1159
+ 37: 'left',
1160
+ 39: 'right',
1161
+ 46: 'delete',
1162
+ 219: '[',
1163
+ 221: ']'
1164
+ };
1165
+
1166
+ // Ref: http://unixpapa.com/js/key.html
1167
+ var onKey = function ( event ) {
1168
+ var code = event.keyCode,
1169
+ key = keys[ code ],
1170
+ modifiers = '',
1171
+ range = this.getSelection();
1172
+
1173
+ if ( event.defaultPrevented ) {
1174
+ return;
1175
+ }
1176
+
1177
+ if ( !key ) {
1178
+ key = String.fromCharCode( code ).toLowerCase();
1179
+ // Only reliable for letters and numbers
1180
+ if ( !/^[A-Za-z0-9]$/.test( key ) ) {
1181
+ key = '';
1182
+ }
1183
+ }
1184
+
1185
+ // On keypress, delete and '.' both have event.keyCode 46
1186
+ // Must check event.which to differentiate.
1187
+ if ( isPresto && event.which === 46 ) {
1188
+ key = '.';
1189
+ }
1190
+
1191
+ // Function keys
1192
+ if ( 111 < code && code < 124 ) {
1193
+ key = 'f' + ( code - 111 );
1194
+ }
1195
+
1196
+ // We need to apply the backspace/delete handlers regardless of
1197
+ // control key modifiers.
1198
+ if ( key !== 'backspace' && key !== 'delete' ) {
1199
+ if ( event.altKey ) { modifiers += 'alt-'; }
1200
+ if ( event.ctrlKey ) { modifiers += 'ctrl-'; }
1201
+ if ( event.metaKey ) { modifiers += 'meta-'; }
1202
+ }
1203
+ // However, on Windows, shift-delete is apparently "cut" (WTF right?), so
1204
+ // we want to let the browser handle shift-delete.
1205
+ if ( event.shiftKey ) { modifiers += 'shift-'; }
1206
+
1207
+ key = modifiers + key;
1208
+
1209
+ if ( this._keyHandlers[ key ] ) {
1210
+ this._keyHandlers[ key ]( this, event, range );
1211
+ } else if ( key.length === 1 && !range.collapsed ) {
1212
+ // Record undo checkpoint.
1213
+ this._recordUndoState( range );
1214
+ this._getRangeAndRemoveBookmark( range );
1215
+ // Delete the selection
1216
+ deleteContentsOfRange( range );
1217
+ this._ensureBottomLine();
1218
+ this.setSelection( range );
1219
+ this._updatePath( range, true );
1220
+ }
1221
+ };
1222
+
1076
1223
  var mapKeyTo = function ( method ) {
1077
1224
  return function ( self, event ) {
1078
1225
  event.preventDefault();
@@ -1129,6 +1276,14 @@ var afterDelete = function ( self, range ) {
1129
1276
  // Move cursor into text node
1130
1277
  moveRangeBoundariesDownTree( range );
1131
1278
  }
1279
+ // If you delete the last character in the sole <div> in Chrome,
1280
+ // it removes the div and replaces it with just a <br> inside the
1281
+ // body. Detach the <br>; the _ensureBottomLine call will insert a new
1282
+ // block.
1283
+ if ( node.nodeName === 'BODY' &&
1284
+ ( node = node.firstChild ) && node.nodeName === 'BR' ) {
1285
+ detach( node );
1286
+ }
1132
1287
  self._ensureBottomLine();
1133
1288
  self.setSelection( range );
1134
1289
  self._updatePath( range, true );
@@ -1240,11 +1395,9 @@ var keyHandlers = {
1240
1395
  if ( nodeAfterSplit.nodeType === TEXT_NODE ) {
1241
1396
  nodeAfterSplit = nodeAfterSplit.parentNode;
1242
1397
  }
1243
- var doc = self._doc,
1244
- body = self._body;
1245
- if ( nodeAfterSplit.offsetTop + nodeAfterSplit.offsetHeight >
1246
- ( doc.documentElement.scrollTop || body.scrollTop ) +
1247
- body.offsetHeight ) {
1398
+ // 16 ~ one standard line height in px.
1399
+ if ( nodeAfterSplit.getBoundingClientRect().top + 16 >
1400
+ self._doc.documentElement.clientHeight ) {
1248
1401
  nodeAfterSplit.scrollIntoView( false );
1249
1402
  }
1250
1403
  },
@@ -1348,17 +1501,33 @@ var keyHandlers = {
1348
1501
  // Otherwise, leave to browser but check afterwards whether it has
1349
1502
  // left behind an empty inline tag.
1350
1503
  else {
1351
- self.setSelection( range );
1504
+ // But first check if the cursor is just before an IMG tag. If so,
1505
+ // delete it ourselves, because the browser won't if it is not
1506
+ // inline.
1507
+ var originalRange = range.cloneRange(),
1508
+ cursorContainer, cursorOffset, nodeAfterCursor;
1509
+ moveRangeBoundariesUpTree( range, self._body );
1510
+ cursorContainer = range.endContainer;
1511
+ cursorOffset = range.endOffset;
1512
+ if ( cursorContainer.nodeType === ELEMENT_NODE ) {
1513
+ nodeAfterCursor = cursorContainer.childNodes[ cursorOffset ];
1514
+ if ( nodeAfterCursor && nodeAfterCursor.nodeName === 'IMG' ) {
1515
+ event.preventDefault();
1516
+ detach( nodeAfterCursor );
1517
+ moveRangeBoundariesDownTree( range );
1518
+ afterDelete( self, range );
1519
+ return;
1520
+ }
1521
+ }
1522
+ self.setSelection( originalRange );
1352
1523
  setTimeout( function () { afterDelete( self ); }, 0 );
1353
1524
  }
1354
1525
  },
1355
1526
  tab: function ( self, event, range ) {
1356
1527
  var node, parent;
1357
1528
  self._removeZWS();
1358
- // If no selection and in an empty block
1359
- if ( range.collapsed &&
1360
- rangeDoesStartAtBlockBoundary( range ) &&
1361
- rangeDoesEndAtBlockBoundary( range ) ) {
1529
+ // If no selection and at start of block
1530
+ if ( range.collapsed && rangeDoesStartAtBlockBoundary( range ) ) {
1362
1531
  node = getStartBlockOfRange( range );
1363
1532
  // Iterate through the block's parents
1364
1533
  while ( parent = node.parentNode ) {
@@ -1374,7 +1543,18 @@ var keyHandlers = {
1374
1543
  }
1375
1544
  node = parent;
1376
1545
  }
1377
- event.preventDefault();
1546
+ }
1547
+ },
1548
+ 'shift-tab': function ( self, event, range ) {
1549
+ self._removeZWS();
1550
+ // If no selection and at start of block
1551
+ if ( range.collapsed && rangeDoesStartAtBlockBoundary( range ) ) {
1552
+ // Break list
1553
+ var node = range.startContainer;
1554
+ if ( getNearest( node, 'UL' ) || getNearest( node, 'OL' ) ) {
1555
+ event.preventDefault();
1556
+ self.modifyBlocks( decreaseListLevel, range );
1557
+ }
1378
1558
  }
1379
1559
  },
1380
1560
  space: function ( self, _, range ) {
@@ -1418,6 +1598,18 @@ if ( isMac && isGecko && win.getSelection().modify ) {
1418
1598
  };
1419
1599
  }
1420
1600
 
1601
+ // System standard for page up/down on Mac is to just scroll, not move the
1602
+ // cursor. On Linux/Windows, it should move the cursor, but some browsers don't
1603
+ // implement this natively. Override to support it.
1604
+ if ( !isMac ) {
1605
+ keyHandlers.pageup = function ( self ) {
1606
+ self.moveCursorToStart();
1607
+ };
1608
+ keyHandlers.pagedown = function ( self ) {
1609
+ self.moveCursorToEnd();
1610
+ };
1611
+ }
1612
+
1421
1613
  keyHandlers[ ctrlKey + 'b' ] = mapKeyToFormat( 'B' );
1422
1614
  keyHandlers[ ctrlKey + 'i' ] = mapKeyToFormat( 'I' );
1423
1615
  keyHandlers[ ctrlKey + 'u' ] = mapKeyToFormat( 'U' );
@@ -1432,1815 +1624,1819 @@ keyHandlers[ ctrlKey + 'y' ] = mapKeyTo( 'redo' );
1432
1624
  keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( 'undo' );
1433
1625
  keyHandlers[ ctrlKey + 'shift-z' ] = mapKeyTo( 'redo' );
1434
1626
 
1435
- var instances = [];
1627
+ var fontSizes = {
1628
+ 1: 10,
1629
+ 2: 13,
1630
+ 3: 16,
1631
+ 4: 18,
1632
+ 5: 24,
1633
+ 6: 32,
1634
+ 7: 48
1635
+ };
1436
1636
 
1437
- function getSquireInstance ( doc ) {
1438
- var l = instances.length,
1439
- instance;
1440
- while ( l-- ) {
1441
- instance = instances[l];
1442
- if ( instance._doc === doc ) {
1443
- return instance;
1637
+ var spanToSemantic = {
1638
+ backgroundColor: {
1639
+ regexp: notWS,
1640
+ replace: function ( doc, colour ) {
1641
+ return createElement( doc, 'SPAN', {
1642
+ 'class': 'highlight',
1643
+ style: 'background-color:' + colour
1644
+ });
1645
+ }
1646
+ },
1647
+ color: {
1648
+ regexp: notWS,
1649
+ replace: function ( doc, colour ) {
1650
+ return createElement( doc, 'SPAN', {
1651
+ 'class': 'colour',
1652
+ style: 'color:' + colour
1653
+ });
1654
+ }
1655
+ },
1656
+ fontWeight: {
1657
+ regexp: /^bold/i,
1658
+ replace: function ( doc ) {
1659
+ return createElement( doc, 'B' );
1660
+ }
1661
+ },
1662
+ fontStyle: {
1663
+ regexp: /^italic/i,
1664
+ replace: function ( doc ) {
1665
+ return createElement( doc, 'I' );
1666
+ }
1667
+ },
1668
+ fontFamily: {
1669
+ regexp: notWS,
1670
+ replace: function ( doc, family ) {
1671
+ return createElement( doc, 'SPAN', {
1672
+ 'class': 'font',
1673
+ style: 'font-family:' + family
1674
+ });
1675
+ }
1676
+ },
1677
+ fontSize: {
1678
+ regexp: notWS,
1679
+ replace: function ( doc, size ) {
1680
+ return createElement( doc, 'SPAN', {
1681
+ 'class': 'size',
1682
+ style: 'font-size:' + size
1683
+ });
1444
1684
  }
1445
1685
  }
1446
- return null;
1447
- }
1448
-
1449
- function mergeObjects ( base, extras ) {
1450
- var prop, value;
1451
- if ( !base ) {
1452
- base = {};
1453
- }
1454
- for ( prop in extras ) {
1455
- value = extras[ prop ];
1456
- base[ prop ] = ( value && value.constructor === Object ) ?
1457
- mergeObjects( base[ prop ], value ) :
1458
- value;
1459
- }
1460
- return base;
1461
- }
1686
+ };
1462
1687
 
1463
- function Squire ( doc, config ) {
1464
- var win = doc.defaultView;
1465
- var body = doc.body;
1466
- var mutation;
1688
+ var replaceWithTag = function ( tag ) {
1689
+ return function ( node, parent ) {
1690
+ var el = createElement( node.ownerDocument, tag );
1691
+ parent.replaceChild( el, node );
1692
+ el.appendChild( empty( node ) );
1693
+ return el;
1694
+ };
1695
+ };
1467
1696
 
1468
- this._win = win;
1469
- this._doc = doc;
1470
- this._body = body;
1697
+ var stylesRewriters = {
1698
+ SPAN: function ( span, parent ) {
1699
+ var style = span.style,
1700
+ doc = span.ownerDocument,
1701
+ attr, converter, css, newTreeBottom, newTreeTop, el;
1471
1702
 
1472
- this._events = {};
1703
+ for ( attr in spanToSemantic ) {
1704
+ converter = spanToSemantic[ attr ];
1705
+ css = style[ attr ];
1706
+ if ( css && converter.regexp.test( css ) ) {
1707
+ el = converter.replace( doc, css );
1708
+ if ( newTreeBottom ) {
1709
+ newTreeBottom.appendChild( el );
1710
+ }
1711
+ newTreeBottom = el;
1712
+ if ( !newTreeTop ) {
1713
+ newTreeTop = el;
1714
+ }
1715
+ }
1716
+ }
1473
1717
 
1474
- this._sel = win.getSelection();
1475
- this._lastSelection = null;
1718
+ if ( newTreeTop ) {
1719
+ newTreeBottom.appendChild( empty( span ) );
1720
+ parent.replaceChild( newTreeTop, span );
1721
+ }
1476
1722
 
1477
- // IE loses selection state of iframe on blur, so make sure we
1478
- // cache it just before it loses focus.
1479
- if ( losesSelectionOnBlur ) {
1480
- this.addEventListener( 'beforedeactivate', this.getSelection );
1723
+ return newTreeBottom || span;
1724
+ },
1725
+ STRONG: replaceWithTag( 'B' ),
1726
+ EM: replaceWithTag( 'I' ),
1727
+ STRIKE: replaceWithTag( 'S' ),
1728
+ FONT: function ( node, parent ) {
1729
+ var face = node.face,
1730
+ size = node.size,
1731
+ colour = node.color,
1732
+ doc = node.ownerDocument,
1733
+ fontSpan, sizeSpan, colourSpan,
1734
+ newTreeBottom, newTreeTop;
1735
+ if ( face ) {
1736
+ fontSpan = createElement( doc, 'SPAN', {
1737
+ 'class': 'font',
1738
+ style: 'font-family:' + face
1739
+ });
1740
+ newTreeTop = fontSpan;
1741
+ newTreeBottom = fontSpan;
1742
+ }
1743
+ if ( size ) {
1744
+ sizeSpan = createElement( doc, 'SPAN', {
1745
+ 'class': 'size',
1746
+ style: 'font-size:' + fontSizes[ size ] + 'px'
1747
+ });
1748
+ if ( !newTreeTop ) {
1749
+ newTreeTop = sizeSpan;
1750
+ }
1751
+ if ( newTreeBottom ) {
1752
+ newTreeBottom.appendChild( sizeSpan );
1753
+ }
1754
+ newTreeBottom = sizeSpan;
1755
+ }
1756
+ if ( colour && /^#?([\dA-F]{3}){1,2}$/i.test( colour ) ) {
1757
+ if ( colour.charAt( 0 ) !== '#' ) {
1758
+ colour = '#' + colour;
1759
+ }
1760
+ colourSpan = createElement( doc, 'SPAN', {
1761
+ 'class': 'colour',
1762
+ style: 'color:' + colour
1763
+ });
1764
+ if ( !newTreeTop ) {
1765
+ newTreeTop = colourSpan;
1766
+ }
1767
+ if ( newTreeBottom ) {
1768
+ newTreeBottom.appendChild( colourSpan );
1769
+ }
1770
+ newTreeBottom = colourSpan;
1771
+ }
1772
+ if ( !newTreeTop ) {
1773
+ newTreeTop = newTreeBottom = createElement( doc, 'SPAN' );
1774
+ }
1775
+ parent.replaceChild( newTreeTop, node );
1776
+ newTreeBottom.appendChild( empty( node ) );
1777
+ return newTreeBottom;
1778
+ },
1779
+ TT: function ( node, parent ) {
1780
+ var el = createElement( node.ownerDocument, 'SPAN', {
1781
+ 'class': 'font',
1782
+ style: 'font-family:menlo,consolas,"courier new",monospace'
1783
+ });
1784
+ parent.replaceChild( el, node );
1785
+ el.appendChild( empty( node ) );
1786
+ return el;
1481
1787
  }
1788
+ };
1482
1789
 
1483
- this._hasZWS = false;
1790
+ var allowedBlock = /^(?:A(?:DDRESS|RTICLE|SIDE|UDIO)|BLOCKQUOTE|CAPTION|D(?:[DLT]|IV)|F(?:IGURE|IGCAPTION|OOTER)|H[1-6]|HEADER|L(?:ABEL|EGEND|I)|O(?:L|UTPUT)|P(?:RE)?|SECTION|T(?:ABLE|BODY|D|FOOT|H|HEAD|R)|UL)$/;
1484
1791
 
1485
- this._lastAnchorNode = null;
1486
- this._lastFocusNode = null;
1487
- this._path = '';
1792
+ var blacklist = /^(?:HEAD|META|STYLE)/;
1488
1793
 
1489
- this.addEventListener( 'keyup', this._updatePathOnEvent );
1490
- this.addEventListener( 'mouseup', this._updatePathOnEvent );
1794
+ var walker = new TreeWalker( null, SHOW_TEXT|SHOW_ELEMENT, function () {
1795
+ return true;
1796
+ });
1491
1797
 
1492
- win.addEventListener( 'focus', this, false );
1493
- win.addEventListener( 'blur', this, false );
1798
+ /*
1799
+ Two purposes:
1494
1800
 
1495
- this._undoIndex = -1;
1496
- this._undoStack = [];
1497
- this._undoStackLength = 0;
1498
- this._isInUndoState = false;
1499
- this._ignoreChange = false;
1801
+ 1. Remove nodes we don't want, such as weird <o:p> tags, comment nodes
1802
+ and whitespace nodes.
1803
+ 2. Convert inline tags into our preferred format.
1804
+ */
1805
+ var cleanTree = function cleanTree ( node ) {
1806
+ var children = node.childNodes,
1807
+ nonInlineParent, i, l, child, nodeName, nodeType, rewriter, childLength,
1808
+ startsWithWS, endsWithWS, data, sibling;
1500
1809
 
1501
- if ( canObserveMutations ) {
1502
- mutation = new MutationObserver( this._docWasChanged.bind( this ) );
1503
- mutation.observe( body, {
1504
- childList: true,
1505
- attributes: true,
1506
- characterData: true,
1507
- subtree: true
1508
- });
1509
- this._mutation = mutation;
1510
- } else {
1511
- this.addEventListener( 'keyup', this._keyUpDetectChange );
1810
+ nonInlineParent = node;
1811
+ while ( isInline( nonInlineParent ) ) {
1812
+ nonInlineParent = nonInlineParent.parentNode;
1512
1813
  }
1814
+ walker.root = nonInlineParent;
1513
1815
 
1514
- // IE sometimes fires the beforepaste event twice; make sure it is not run
1515
- // again before our after paste function is called.
1516
- this._awaitingPaste = false;
1517
- this.addEventListener( isIElt11 ? 'beforecut' : 'cut', this._onCut );
1518
- this.addEventListener( isIElt11 ? 'beforepaste' : 'paste', this._onPaste );
1519
-
1520
- // Opera does not fire keydown repeatedly.
1521
- this.addEventListener( isPresto ? 'keypress' : 'keydown', this._onKey );
1522
-
1523
- // Add key handlers
1524
- this._keyHandlers = Object.create( keyHandlers );
1525
-
1526
- // Override default properties
1527
- this.setConfig( config );
1528
-
1529
- // Fix IE<10's buggy implementation of Text#splitText.
1530
- // If the split is at the end of the node, it doesn't insert the newly split
1531
- // node into the document, and sets its value to undefined rather than ''.
1532
- // And even if the split is not at the end, the original node is removed
1533
- // from the document and replaced by another, rather than just having its
1534
- // data shortened.
1535
- // We used to feature test for this, but then found the feature test would
1536
- // sometimes pass, but later on the buggy behaviour would still appear.
1537
- // I think IE10 does not have the same bug, but it doesn't hurt to replace
1538
- // its native fn too and then we don't need yet another UA category.
1539
- if ( isIElt11 ) {
1540
- win.Text.prototype.splitText = function ( offset ) {
1541
- var afterSplit = this.ownerDocument.createTextNode(
1542
- this.data.slice( offset ) ),
1543
- next = this.nextSibling,
1544
- parent = this.parentNode,
1545
- toDelete = this.length - offset;
1546
- if ( next ) {
1547
- parent.insertBefore( afterSplit, next );
1548
- } else {
1549
- parent.appendChild( afterSplit );
1816
+ for ( i = 0, l = children.length; i < l; i += 1 ) {
1817
+ child = children[i];
1818
+ nodeName = child.nodeName;
1819
+ nodeType = child.nodeType;
1820
+ rewriter = stylesRewriters[ nodeName ];
1821
+ if ( nodeType === ELEMENT_NODE ) {
1822
+ childLength = child.childNodes.length;
1823
+ if ( rewriter ) {
1824
+ child = rewriter( child, node );
1825
+ } else if ( blacklist.test( nodeName ) ) {
1826
+ node.removeChild( child );
1827
+ i -= 1;
1828
+ l -= 1;
1829
+ continue;
1830
+ } else if ( !allowedBlock.test( nodeName ) && !isInline( child ) ) {
1831
+ i -= 1;
1832
+ l += childLength - 1;
1833
+ node.replaceChild( empty( child ), child );
1834
+ continue;
1550
1835
  }
1551
- if ( toDelete ) {
1552
- this.deleteData( offset, toDelete );
1836
+ if ( childLength ) {
1837
+ cleanTree( child );
1553
1838
  }
1554
- return afterSplit;
1555
- };
1839
+ } else {
1840
+ if ( nodeType === TEXT_NODE ) {
1841
+ data = child.data;
1842
+ startsWithWS = !notWS.test( data.charAt( 0 ) );
1843
+ endsWithWS = !notWS.test( data.charAt( data.length - 1 ) );
1844
+ if ( !startsWithWS && !endsWithWS ) {
1845
+ continue;
1846
+ }
1847
+ // Iterate through the nodes; if we hit some other content
1848
+ // before the start of a new block we don't trim
1849
+ if ( startsWithWS ) {
1850
+ walker.currentNode = child;
1851
+ while ( sibling = walker.previousPONode() ) {
1852
+ nodeName = sibling.nodeName;
1853
+ if ( nodeName === 'IMG' ||
1854
+ ( nodeName === '#text' &&
1855
+ /\S/.test( sibling.data ) ) ) {
1856
+ break;
1857
+ }
1858
+ if ( !isInline( sibling ) ) {
1859
+ sibling = null;
1860
+ break;
1861
+ }
1862
+ }
1863
+ if ( !sibling ) {
1864
+ data = data.replace( /^\s+/g, '' );
1865
+ }
1866
+ }
1867
+ if ( endsWithWS ) {
1868
+ walker.currentNode = child;
1869
+ while ( sibling = walker.nextNode() ) {
1870
+ if ( nodeName === 'IMG' ||
1871
+ ( nodeName === '#text' &&
1872
+ /\S/.test( sibling.data ) ) ) {
1873
+ break;
1874
+ }
1875
+ if ( !isInline( sibling ) ) {
1876
+ sibling = null;
1877
+ break;
1878
+ }
1879
+ }
1880
+ if ( !sibling ) {
1881
+ data = data.replace( /^\s+/g, '' );
1882
+ }
1883
+ }
1884
+ if ( data ) {
1885
+ child.data = data;
1886
+ continue;
1887
+ }
1888
+ }
1889
+ node.removeChild( child );
1890
+ i -= 1;
1891
+ l -= 1;
1892
+ }
1556
1893
  }
1894
+ return node;
1895
+ };
1557
1896
 
1558
- body.setAttribute( 'contenteditable', 'true' );
1559
-
1560
- // Remove Firefox's built-in controls
1561
- try {
1562
- doc.execCommand( 'enableObjectResizing', false, 'false' );
1563
- doc.execCommand( 'enableInlineTableEditing', false, 'false' );
1564
- } catch ( error ) {}
1565
-
1566
- instances.push( this );
1567
-
1568
- // Need to register instance before calling setHTML, so that the fixCursor
1569
- // function can lookup any default block tag options set.
1570
- this.setHTML( '' );
1571
- }
1572
-
1573
- var proto = Squire.prototype;
1897
+ // ---
1574
1898
 
1575
- proto.setConfig = function ( config ) {
1576
- config = mergeObjects({
1577
- blockTag: 'DIV',
1578
- blockAttributes: null,
1579
- tagAttributes: {
1580
- blockquote: null,
1581
- ul: null,
1582
- ol: null,
1583
- li: null
1899
+ var removeEmptyInlines = function removeEmptyInlines ( root ) {
1900
+ var children = root.childNodes,
1901
+ l = children.length,
1902
+ child;
1903
+ while ( l-- ) {
1904
+ child = children[l];
1905
+ if ( child.nodeType === ELEMENT_NODE && !isLeaf( child ) ) {
1906
+ removeEmptyInlines( child );
1907
+ if ( isInline( child ) && !child.firstChild ) {
1908
+ root.removeChild( child );
1909
+ }
1910
+ } else if ( child.nodeType === TEXT_NODE && !child.data ) {
1911
+ root.removeChild( child );
1584
1912
  }
1585
- }, config );
1586
-
1587
- // Users may specify block tag in lower case
1588
- config.blockTag = config.blockTag.toUpperCase();
1589
-
1590
- this._config = config;
1591
-
1592
- return this;
1593
- };
1594
-
1595
- proto.createElement = function ( tag, props, children ) {
1596
- return createElement( this._doc, tag, props, children );
1913
+ }
1597
1914
  };
1598
1915
 
1599
- proto.createDefaultBlock = function ( children ) {
1600
- var config = this._config;
1601
- return fixCursor(
1602
- this.createElement( config.blockTag, config.blockAttributes, children )
1603
- );
1604
- };
1916
+ // ---
1605
1917
 
1606
- proto.didError = function ( error ) {
1607
- console.log( error );
1918
+ var notWSTextNode = function ( node ) {
1919
+ return node.nodeType === ELEMENT_NODE ?
1920
+ node.nodeName === 'BR' :
1921
+ notWS.test( node.data );
1608
1922
  };
1609
-
1610
- proto.getDocument = function () {
1611
- return this._doc;
1923
+ var isLineBreak = function ( br ) {
1924
+ var block = br.parentNode,
1925
+ walker;
1926
+ while ( isInline( block ) ) {
1927
+ block = block.parentNode;
1928
+ }
1929
+ walker = new TreeWalker(
1930
+ block, SHOW_ELEMENT|SHOW_TEXT, notWSTextNode );
1931
+ walker.currentNode = br;
1932
+ return !!walker.nextNode();
1612
1933
  };
1613
1934
 
1614
- // --- Events ---
1615
-
1616
- // Subscribing to these events won't automatically add a listener to the
1617
- // document node, since these events are fired in a custom manner by the
1618
- // editor code.
1619
- var customEvents = {
1620
- focus: 1, blur: 1,
1621
- pathChange: 1, select: 1, input: 1, undoStateChange: 1
1622
- };
1623
-
1624
- proto.fireEvent = function ( type, event ) {
1625
- var handlers = this._events[ type ],
1626
- i, l, obj;
1627
- if ( handlers ) {
1628
- if ( !event ) {
1629
- event = {};
1630
- }
1631
- if ( event.type !== type ) {
1632
- event.type = type;
1633
- }
1634
- // Clone handlers array, so any handlers added/removed do not affect it.
1635
- handlers = handlers.slice();
1636
- for ( i = 0, l = handlers.length; i < l; i += 1 ) {
1637
- obj = handlers[i];
1638
- try {
1639
- if ( obj.handleEvent ) {
1640
- obj.handleEvent( event );
1641
- } else {
1642
- obj.call( this, event );
1643
- }
1644
- } catch ( error ) {
1645
- error.details = 'Squire: fireEvent error. Event type: ' + type;
1646
- this.didError( error );
1647
- }
1648
- }
1649
- }
1650
- return this;
1651
- };
1935
+ // <br> elements are treated specially, and differently depending on the
1936
+ // browser, when in rich text editor mode. When adding HTML from external
1937
+ // sources, we must remove them, replacing the ones that actually affect
1938
+ // line breaks by wrapping the inline text in a <div>. Browsers that want <br>
1939
+ // elements at the end of each block will then have them added back in a later
1940
+ // fixCursor method call.
1941
+ var cleanupBRs = function ( root ) {
1942
+ var brs = root.querySelectorAll( 'BR' ),
1943
+ brBreaksLine = [],
1944
+ l = brs.length,
1945
+ i, br, parent;
1652
1946
 
1653
- proto.destroy = function () {
1654
- var win = this._win,
1655
- doc = this._doc,
1656
- events = this._events,
1657
- type;
1658
- win.removeEventListener( 'focus', this, false );
1659
- win.removeEventListener( 'blur', this, false );
1660
- for ( type in events ) {
1661
- if ( !customEvents[ type ] ) {
1662
- doc.removeEventListener( type, this, true );
1663
- }
1664
- }
1665
- if ( this._mutation ) {
1666
- this._mutation.disconnect();
1947
+ // Must calculate whether the <br> breaks a line first, because if we
1948
+ // have two <br>s next to each other, after the first one is converted
1949
+ // to a block split, the second will be at the end of a block and
1950
+ // therefore seem to not be a line break. But in its original context it
1951
+ // was, so we should also convert it to a block split.
1952
+ for ( i = 0; i < l; i += 1 ) {
1953
+ brBreaksLine[i] = isLineBreak( brs[i] );
1667
1954
  }
1668
- var l = instances.length;
1669
1955
  while ( l-- ) {
1670
- if ( instances[l] === this ) {
1671
- instances.splice( l, 1 );
1956
+ br = brs[l];
1957
+ // Cleanup may have removed it
1958
+ parent = br.parentNode;
1959
+ if ( !parent ) { continue; }
1960
+ // If it doesn't break a line, just remove it; it's not doing
1961
+ // anything useful. We'll add it back later if required by the
1962
+ // browser. If it breaks a line, wrap the content in div tags
1963
+ // and replace the brs.
1964
+ if ( !brBreaksLine[l] ) {
1965
+ detach( br );
1966
+ } else if ( !isInline( parent ) ) {
1967
+ fixContainer( parent );
1672
1968
  }
1673
1969
  }
1674
1970
  };
1675
1971
 
1676
- proto.handleEvent = function ( event ) {
1677
- this.fireEvent( event.type, event );
1678
- };
1679
-
1680
- proto.addEventListener = function ( type, fn ) {
1681
- var handlers = this._events[ type ];
1682
- if ( !fn ) {
1683
- this.didError({
1684
- name: 'Squire: addEventListener with null or undefined fn',
1685
- message: 'Event type: ' + type
1686
- });
1687
- return this;
1688
- }
1689
- if ( !handlers ) {
1690
- handlers = this._events[ type ] = [];
1691
- if ( !customEvents[ type ] ) {
1692
- this._doc.addEventListener( type, this, true );
1972
+ var onCut = function () {
1973
+ // Save undo checkpoint
1974
+ var range = this.getSelection();
1975
+ var self = this;
1976
+ this._recordUndoState( range );
1977
+ this._getRangeAndRemoveBookmark( range );
1978
+ this.setSelection( range );
1979
+ setTimeout( function () {
1980
+ try {
1981
+ // If all content removed, ensure div at start of body.
1982
+ self._ensureBottomLine();
1983
+ } catch ( error ) {
1984
+ self.didError( error );
1693
1985
  }
1694
- }
1695
- handlers.push( fn );
1696
- return this;
1986
+ }, 0 );
1697
1987
  };
1698
1988
 
1699
- proto.removeEventListener = function ( type, fn ) {
1700
- var handlers = this._events[ type ],
1701
- l;
1702
- if ( handlers ) {
1703
- l = handlers.length;
1989
+ var onPaste = function ( event ) {
1990
+ var clipboardData = event.clipboardData,
1991
+ items = clipboardData && clipboardData.items,
1992
+ fireDrop = false,
1993
+ hasImage = false,
1994
+ plainItem = null,
1995
+ self = this,
1996
+ l, item, type, data;
1997
+
1998
+ // Current HTML5 Clipboard interface
1999
+ // ---------------------------------
2000
+ // https://html.spec.whatwg.org/multipage/interaction.html
2001
+
2002
+ if ( items ) {
2003
+ event.preventDefault();
2004
+ l = items.length;
1704
2005
  while ( l-- ) {
1705
- if ( handlers[l] === fn ) {
1706
- handlers.splice( l, 1 );
2006
+ item = items[l];
2007
+ type = item.type;
2008
+ if ( type === 'text/html' ) {
2009
+ /*jshint loopfunc: true */
2010
+ item.getAsString( function ( html ) {
2011
+ self.insertHTML( html, true );
2012
+ });
2013
+ /*jshint loopfunc: false */
2014
+ return;
2015
+ }
2016
+ if ( type === 'text/plain' ) {
2017
+ plainItem = item;
2018
+ }
2019
+ if ( /^image\/.*/.test( type ) ) {
2020
+ hasImage = true;
1707
2021
  }
1708
2022
  }
1709
- if ( !handlers.length ) {
1710
- delete this._events[ type ];
1711
- if ( !customEvents[ type ] ) {
1712
- this._doc.removeEventListener( type, this, false );
2023
+ // Treat image paste as a drop of an image file.
2024
+ if ( hasImage ) {
2025
+ this.fireEvent( 'dragover', {
2026
+ dataTransfer: clipboardData,
2027
+ /*jshint loopfunc: true */
2028
+ preventDefault: function () {
2029
+ fireDrop = true;
2030
+ }
2031
+ /*jshint loopfunc: false */
2032
+ });
2033
+ if ( fireDrop ) {
2034
+ this.fireEvent( 'drop', {
2035
+ dataTransfer: clipboardData
2036
+ });
1713
2037
  }
2038
+ } else if ( plainItem ) {
2039
+ item.getAsString( function ( text ) {
2040
+ self.insertPlainText( text, true );
2041
+ });
1714
2042
  }
2043
+ return;
1715
2044
  }
1716
- return this;
1717
- };
1718
-
1719
- // --- Selection and Path ---
1720
2045
 
1721
- proto._createRange =
1722
- function ( range, startOffset, endContainer, endOffset ) {
1723
- if ( range instanceof this._win.Range ) {
1724
- return range.cloneRange();
1725
- }
1726
- var domRange = this._doc.createRange();
1727
- domRange.setStart( range, startOffset );
1728
- if ( endContainer ) {
1729
- domRange.setEnd( endContainer, endOffset );
1730
- } else {
1731
- domRange.setEnd( range, startOffset );
1732
- }
1733
- return domRange;
1734
- };
2046
+ // Old interface
2047
+ // -------------
1735
2048
 
1736
- proto.setSelection = function ( range ) {
1737
- if ( range ) {
1738
- // iOS bug: if you don't focus the iframe before setting the
1739
- // selection, you can end up in a state where you type but the input
1740
- // doesn't get directed into the contenteditable area but is instead
1741
- // lost in a black hole. Very strange.
1742
- if ( isIOS ) {
1743
- this._win.focus();
2049
+ // Safari (and indeed many other OS X apps) copies stuff as text/rtf
2050
+ // rather than text/html; even from a webpage in Safari. The only way
2051
+ // to get an HTML version is to fallback to letting the browser insert
2052
+ // the content. Same for getting image data. *Sigh*.
2053
+ if ( clipboardData && (
2054
+ indexOf.call( clipboardData.types, 'text/html' ) > -1 || (
2055
+ indexOf.call( clipboardData.types, 'text/plain' ) > -1 &&
2056
+ indexOf.call( clipboardData.types, 'text/rtf' ) < 0 ) ) ) {
2057
+ event.preventDefault();
2058
+ // Abiword on Linux copies a plain text and html version, but the HTML
2059
+ // version is the empty string! So always try to get HTML, but if none,
2060
+ // insert plain text instead.
2061
+ if (( data = clipboardData.getData( 'text/html' ) )) {
2062
+ this.insertHTML( data, true );
2063
+ } else if (( data = clipboardData.getData( 'text/plain' ) )) {
2064
+ this.insertPlainText( data, true );
1744
2065
  }
1745
- var sel = this._sel;
1746
- sel.removeAllRanges();
1747
- sel.addRange( range );
2066
+ return;
1748
2067
  }
1749
- return this;
1750
- };
1751
2068
 
1752
- proto.getSelection = function () {
1753
- var sel = this._sel,
1754
- selection, startContainer, endContainer;
1755
- if ( sel.rangeCount ) {
1756
- selection = sel.getRangeAt( 0 ).cloneRange();
1757
- startContainer = selection.startContainer;
1758
- endContainer = selection.endContainer;
1759
- // FF can return the selection as being inside an <img>. WTF?
1760
- if ( startContainer && isLeaf( startContainer ) ) {
1761
- selection.setStartBefore( startContainer );
1762
- }
1763
- if ( endContainer && isLeaf( endContainer ) ) {
1764
- selection.setEndBefore( endContainer );
1765
- }
1766
- this._lastSelection = selection;
1767
- } else {
1768
- selection = this._lastSelection;
1769
- }
1770
- if ( !selection ) {
1771
- selection = this._createRange( this._body.firstChild, 0 );
1772
- }
1773
- return selection;
1774
- };
2069
+ // No interface :(
2070
+ // ---------------
1775
2071
 
1776
- proto.getSelectedText = function () {
1777
- var range = this.getSelection(),
1778
- walker = new TreeWalker(
1779
- range.commonAncestorContainer,
1780
- SHOW_TEXT|SHOW_ELEMENT,
1781
- function ( node ) {
1782
- return isNodeContainedInRange( range, node, true );
1783
- }
1784
- ),
2072
+ this._awaitingPaste = true;
2073
+
2074
+ var body = this._body,
2075
+ range = this.getSelection(),
1785
2076
  startContainer = range.startContainer,
2077
+ startOffset = range.startOffset,
1786
2078
  endContainer = range.endContainer,
1787
- node = walker.currentNode = startContainer,
1788
- textContent = '',
1789
- addedTextInBlock = false,
1790
- value;
2079
+ endOffset = range.endOffset,
2080
+ startBlock = getStartBlockOfRange( range );
1791
2081
 
1792
- if ( !walker.filter( node ) ) {
1793
- node = walker.nextNode();
1794
- }
2082
+ // We need to position the pasteArea in the visible portion of the screen
2083
+ // to stop the browser auto-scrolling.
2084
+ var pasteArea = this.createElement( 'DIV', {
2085
+ style: 'position: absolute; overflow: hidden; top:' +
2086
+ ( body.scrollTop +
2087
+ ( startBlock ? startBlock.getBoundingClientRect().top : 0 ) ) +
2088
+ 'px; right: 150%; width: 1px; height: 1px;'
2089
+ });
2090
+ body.appendChild( pasteArea );
2091
+ range.selectNodeContents( pasteArea );
2092
+ this.setSelection( range );
1795
2093
 
1796
- while ( node ) {
1797
- if ( node.nodeType === TEXT_NODE ) {
1798
- value = node.data;
1799
- if ( value && ( /\S/.test( value ) ) ) {
1800
- if ( node === endContainer ) {
1801
- value = value.slice( 0, range.endOffset );
1802
- }
1803
- if ( node === startContainer ) {
1804
- value = value.slice( range.startOffset );
2094
+ // A setTimeout of 0 means this is added to the back of the
2095
+ // single javascript thread, so it will be executed after the
2096
+ // paste event.
2097
+ setTimeout( function () {
2098
+ try {
2099
+ // IE sometimes fires the beforepaste event twice; make sure it is
2100
+ // not run again before our after paste function is called.
2101
+ self._awaitingPaste = false;
2102
+
2103
+ // Get the pasted content and clean
2104
+ var html = '',
2105
+ next = pasteArea,
2106
+ first, range;
2107
+
2108
+ // #88: Chrome can apparently split the paste area if certain
2109
+ // content is inserted; gather them all up.
2110
+ while ( pasteArea = next ) {
2111
+ next = pasteArea.nextSibling;
2112
+ detach( pasteArea );
2113
+ // Safari and IE like putting extra divs around things.
2114
+ first = pasteArea.firstChild;
2115
+ if ( first && first === pasteArea.lastChild &&
2116
+ first.nodeName === 'DIV' ) {
2117
+ pasteArea = first;
1805
2118
  }
1806
- textContent += value;
1807
- addedTextInBlock = true;
2119
+ html += pasteArea.innerHTML;
1808
2120
  }
1809
- } else if ( node.nodeName === 'BR' ||
1810
- addedTextInBlock && !isInline( node ) ) {
1811
- textContent += '\n';
1812
- addedTextInBlock = false;
1813
- }
1814
- node = walker.nextNode();
1815
- }
1816
2121
 
1817
- return textContent;
1818
- };
2122
+ range = self._createRange(
2123
+ startContainer, startOffset, endContainer, endOffset );
2124
+ self.setSelection( range );
1819
2125
 
1820
- proto.getPath = function () {
1821
- return this._path;
2126
+ if ( html ) {
2127
+ self.insertHTML( html, true );
2128
+ }
2129
+ } catch ( error ) {
2130
+ self.didError( error );
2131
+ }
2132
+ }, 0 );
1822
2133
  };
1823
2134
 
1824
- // --- Workaround for browsers that can't focus empty text nodes ---
1825
-
1826
- // WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=15256
2135
+ var instances = [];
1827
2136
 
1828
- var removeZWS = function ( root ) {
1829
- var walker = new TreeWalker( root, SHOW_TEXT, function () {
1830
- return true;
1831
- }, false ),
1832
- parent, node, index;
1833
- while ( node = walker.nextNode() ) {
1834
- while ( ( index = node.data.indexOf( ZWS ) ) > -1 ) {
1835
- if ( node.length === 1 ) {
1836
- do {
1837
- parent = node.parentNode;
1838
- parent.removeChild( node );
1839
- node = parent;
1840
- } while ( isInline( node ) && !getLength( node ) );
1841
- break;
1842
- } else {
1843
- node.deleteData( index, 1 );
1844
- }
2137
+ function getSquireInstance ( doc ) {
2138
+ var l = instances.length,
2139
+ instance;
2140
+ while ( l-- ) {
2141
+ instance = instances[l];
2142
+ if ( instance._doc === doc ) {
2143
+ return instance;
1845
2144
  }
1846
2145
  }
1847
- };
2146
+ return null;
2147
+ }
1848
2148
 
1849
- proto._didAddZWS = function () {
1850
- this._hasZWS = true;
1851
- };
1852
- proto._removeZWS = function () {
1853
- if ( !this._hasZWS ) {
1854
- return;
2149
+ function mergeObjects ( base, extras ) {
2150
+ var prop, value;
2151
+ if ( !base ) {
2152
+ base = {};
1855
2153
  }
1856
- removeZWS( this._body );
1857
- this._hasZWS = false;
1858
- };
2154
+ for ( prop in extras ) {
2155
+ value = extras[ prop ];
2156
+ base[ prop ] = ( value && value.constructor === Object ) ?
2157
+ mergeObjects( base[ prop ], value ) :
2158
+ value;
2159
+ }
2160
+ return base;
2161
+ }
1859
2162
 
1860
- // --- Path change events ---
2163
+ function Squire ( doc, config ) {
2164
+ var win = doc.defaultView;
2165
+ var body = doc.body;
2166
+ var mutation;
1861
2167
 
1862
- proto._updatePath = function ( range, force ) {
1863
- var anchor = range.startContainer,
1864
- focus = range.endContainer,
1865
- newPath;
1866
- if ( force || anchor !== this._lastAnchorNode ||
1867
- focus !== this._lastFocusNode ) {
1868
- this._lastAnchorNode = anchor;
1869
- this._lastFocusNode = focus;
1870
- newPath = ( anchor && focus ) ? ( anchor === focus ) ?
1871
- getPath( focus ) : '(selection)' : '';
1872
- if ( this._path !== newPath ) {
1873
- this._path = newPath;
1874
- this.fireEvent( 'pathChange', { path: newPath } );
1875
- }
1876
- }
1877
- if ( !range.collapsed ) {
1878
- this.fireEvent( 'select' );
1879
- }
1880
- };
2168
+ this._win = win;
2169
+ this._doc = doc;
2170
+ this._body = body;
1881
2171
 
1882
- proto._updatePathOnEvent = function () {
1883
- this._updatePath( this.getSelection() );
1884
- };
2172
+ this._events = {};
1885
2173
 
1886
- // --- Focus ---
2174
+ this._lastSelection = null;
1887
2175
 
1888
- proto.focus = function () {
1889
- // FF seems to need the body to be focussed (at least on first load).
1890
- // Chrome also now needs body to be focussed in order to show the cursor
1891
- // (otherwise it is focussed, but the cursor doesn't appear).
1892
- // Opera (Presto-variant) however will lose the selection if you call this!
1893
- if ( !isPresto ) {
1894
- this._body.focus();
2176
+ // IE loses selection state of iframe on blur, so make sure we
2177
+ // cache it just before it loses focus.
2178
+ if ( losesSelectionOnBlur ) {
2179
+ this.addEventListener( 'beforedeactivate', this.getSelection );
1895
2180
  }
1896
- this._win.focus();
1897
- return this;
1898
- };
1899
2181
 
1900
- proto.blur = function () {
1901
- // IE will remove the whole browser window from focus if you call
1902
- // win.blur() or body.blur(), so instead we call top.focus() to focus
1903
- // the top frame, thus blurring this frame. This works in everything
1904
- // except FF, so we need to call body.blur() in that as well.
1905
- if ( isGecko ) {
1906
- this._body.blur();
1907
- }
1908
- top.focus();
1909
- return this;
1910
- };
2182
+ this._hasZWS = false;
1911
2183
 
1912
- // --- Bookmarking ---
2184
+ this._lastAnchorNode = null;
2185
+ this._lastFocusNode = null;
2186
+ this._path = '';
1913
2187
 
1914
- var startSelectionId = 'squire-selection-start';
1915
- var endSelectionId = 'squire-selection-end';
2188
+ this.addEventListener( 'keyup', this._updatePathOnEvent );
2189
+ this.addEventListener( 'mouseup', this._updatePathOnEvent );
1916
2190
 
1917
- proto._saveRangeToBookmark = function ( range ) {
1918
- var startNode = this.createElement( 'INPUT', {
1919
- id: startSelectionId,
1920
- type: 'hidden'
1921
- }),
1922
- endNode = this.createElement( 'INPUT', {
1923
- id: endSelectionId,
1924
- type: 'hidden'
1925
- }),
1926
- temp;
2191
+ win.addEventListener( 'focus', this, false );
2192
+ win.addEventListener( 'blur', this, false );
1927
2193
 
1928
- insertNodeInRange( range, startNode );
1929
- range.collapse( false );
1930
- insertNodeInRange( range, endNode );
2194
+ this._undoIndex = -1;
2195
+ this._undoStack = [];
2196
+ this._undoStackLength = 0;
2197
+ this._isInUndoState = false;
2198
+ this._ignoreChange = false;
1931
2199
 
1932
- // In a collapsed range, the start is sometimes inserted after the end!
1933
- if ( startNode.compareDocumentPosition( endNode ) &
1934
- DOCUMENT_POSITION_PRECEDING ) {
1935
- startNode.id = endSelectionId;
1936
- endNode.id = startSelectionId;
1937
- temp = startNode;
1938
- startNode = endNode;
1939
- endNode = temp;
2200
+ if ( canObserveMutations ) {
2201
+ mutation = new MutationObserver( this._docWasChanged.bind( this ) );
2202
+ mutation.observe( body, {
2203
+ childList: true,
2204
+ attributes: true,
2205
+ characterData: true,
2206
+ subtree: true
2207
+ });
2208
+ this._mutation = mutation;
2209
+ } else {
2210
+ this.addEventListener( 'keyup', this._keyUpDetectChange );
1940
2211
  }
1941
2212
 
1942
- range.setStartAfter( startNode );
1943
- range.setEndBefore( endNode );
1944
- };
2213
+ // IE sometimes fires the beforepaste event twice; make sure it is not run
2214
+ // again before our after paste function is called.
2215
+ this._awaitingPaste = false;
2216
+ this.addEventListener( isIElt11 ? 'beforecut' : 'cut', onCut );
2217
+ this.addEventListener( isIElt11 ? 'beforepaste' : 'paste', onPaste );
1945
2218
 
1946
- proto._getRangeAndRemoveBookmark = function ( range ) {
1947
- var doc = this._doc,
1948
- start = doc.getElementById( startSelectionId ),
1949
- end = doc.getElementById( endSelectionId );
2219
+ // Opera does not fire keydown repeatedly.
2220
+ this.addEventListener( isPresto ? 'keypress' : 'keydown', onKey );
1950
2221
 
1951
- if ( start && end ) {
1952
- var startContainer = start.parentNode,
1953
- endContainer = end.parentNode,
1954
- collapsed;
2222
+ // Add key handlers
2223
+ this._keyHandlers = Object.create( keyHandlers );
1955
2224
 
1956
- var _range = {
1957
- startContainer: startContainer,
1958
- endContainer: endContainer,
1959
- startOffset: indexOf.call( startContainer.childNodes, start ),
1960
- endOffset: indexOf.call( endContainer.childNodes, end )
1961
- };
2225
+ // Override default properties
2226
+ this.setConfig( config );
1962
2227
 
1963
- if ( startContainer === endContainer ) {
1964
- _range.endOffset -= 1;
1965
- }
2228
+ // Fix IE<10's buggy implementation of Text#splitText.
2229
+ // If the split is at the end of the node, it doesn't insert the newly split
2230
+ // node into the document, and sets its value to undefined rather than ''.
2231
+ // And even if the split is not at the end, the original node is removed
2232
+ // from the document and replaced by another, rather than just having its
2233
+ // data shortened.
2234
+ // We used to feature test for this, but then found the feature test would
2235
+ // sometimes pass, but later on the buggy behaviour would still appear.
2236
+ // I think IE10 does not have the same bug, but it doesn't hurt to replace
2237
+ // its native fn too and then we don't need yet another UA category.
2238
+ if ( isIElt11 ) {
2239
+ win.Text.prototype.splitText = function ( offset ) {
2240
+ var afterSplit = this.ownerDocument.createTextNode(
2241
+ this.data.slice( offset ) ),
2242
+ next = this.nextSibling,
2243
+ parent = this.parentNode,
2244
+ toDelete = this.length - offset;
2245
+ if ( next ) {
2246
+ parent.insertBefore( afterSplit, next );
2247
+ } else {
2248
+ parent.appendChild( afterSplit );
2249
+ }
2250
+ if ( toDelete ) {
2251
+ this.deleteData( offset, toDelete );
2252
+ }
2253
+ return afterSplit;
2254
+ };
2255
+ }
1966
2256
 
1967
- detach( start );
1968
- detach( end );
2257
+ body.setAttribute( 'contenteditable', 'true' );
1969
2258
 
1970
- // Merge any text nodes we split
1971
- mergeInlines( startContainer, _range );
1972
- if ( startContainer !== endContainer ) {
1973
- mergeInlines( endContainer, _range );
1974
- }
2259
+ // Remove Firefox's built-in controls
2260
+ try {
2261
+ doc.execCommand( 'enableObjectResizing', false, 'false' );
2262
+ doc.execCommand( 'enableInlineTableEditing', false, 'false' );
2263
+ } catch ( error ) {}
1975
2264
 
1976
- if ( !range ) {
1977
- range = doc.createRange();
1978
- }
1979
- range.setStart( _range.startContainer, _range.startOffset );
1980
- range.setEnd( _range.endContainer, _range.endOffset );
1981
- collapsed = range.collapsed;
2265
+ instances.push( this );
1982
2266
 
1983
- moveRangeBoundariesDownTree( range );
1984
- if ( collapsed ) {
1985
- range.collapse( true );
1986
- }
1987
- }
1988
- return range || null;
1989
- };
2267
+ // Need to register instance before calling setHTML, so that the fixCursor
2268
+ // function can lookup any default block tag options set.
2269
+ this.setHTML( '' );
2270
+ }
1990
2271
 
1991
- // --- Undo ---
2272
+ var proto = Squire.prototype;
1992
2273
 
1993
- proto._keyUpDetectChange = function ( event ) {
1994
- var code = event.keyCode;
1995
- // Presume document was changed if:
1996
- // 1. A modifier key (other than shift) wasn't held down
1997
- // 2. The key pressed is not in range 16<=x<=20 (control keys)
1998
- // 3. The key pressed is not in range 33<=x<=45 (navigation keys)
1999
- if ( !event.ctrlKey && !event.metaKey && !event.altKey &&
2000
- ( code < 16 || code > 20 ) &&
2001
- ( code < 33 || code > 45 ) ) {
2002
- this._docWasChanged();
2003
- }
2274
+ proto.setConfig = function ( config ) {
2275
+ config = mergeObjects({
2276
+ blockTag: 'DIV',
2277
+ blockAttributes: null,
2278
+ tagAttributes: {
2279
+ blockquote: null,
2280
+ ul: null,
2281
+ ol: null,
2282
+ li: null
2283
+ }
2284
+ }, config );
2285
+
2286
+ // Users may specify block tag in lower case
2287
+ config.blockTag = config.blockTag.toUpperCase();
2288
+
2289
+ this._config = config;
2290
+
2291
+ return this;
2004
2292
  };
2005
2293
 
2006
- proto._docWasChanged = function () {
2007
- if ( canObserveMutations && this._ignoreChange ) {
2008
- this._ignoreChange = false;
2009
- return;
2010
- }
2011
- if ( this._isInUndoState ) {
2012
- this._isInUndoState = false;
2013
- this.fireEvent( 'undoStateChange', {
2014
- canUndo: true,
2015
- canRedo: false
2016
- });
2017
- }
2018
- this.fireEvent( 'input' );
2294
+ proto.createElement = function ( tag, props, children ) {
2295
+ return createElement( this._doc, tag, props, children );
2019
2296
  };
2020
2297
 
2021
- // Leaves bookmark
2022
- proto._recordUndoState = function ( range ) {
2023
- // Don't record if we're already in an undo state
2024
- if ( !this._isInUndoState ) {
2025
- // Advance pointer to new position
2026
- var undoIndex = this._undoIndex += 1,
2027
- undoStack = this._undoStack;
2298
+ proto.createDefaultBlock = function ( children ) {
2299
+ var config = this._config;
2300
+ return fixCursor(
2301
+ this.createElement( config.blockTag, config.blockAttributes, children )
2302
+ );
2303
+ };
2028
2304
 
2029
- // Truncate stack if longer (i.e. if has been previously undone)
2030
- if ( undoIndex < this._undoStackLength) {
2031
- undoStack.length = this._undoStackLength = undoIndex;
2305
+ proto.didError = function ( error ) {
2306
+ console.log( error );
2307
+ };
2308
+
2309
+ proto.getDocument = function () {
2310
+ return this._doc;
2311
+ };
2312
+
2313
+ // --- Events ---
2314
+
2315
+ // Subscribing to these events won't automatically add a listener to the
2316
+ // document node, since these events are fired in a custom manner by the
2317
+ // editor code.
2318
+ var customEvents = {
2319
+ focus: 1, blur: 1,
2320
+ pathChange: 1, select: 1, input: 1, undoStateChange: 1
2321
+ };
2322
+
2323
+ proto.fireEvent = function ( type, event ) {
2324
+ var handlers = this._events[ type ],
2325
+ l, obj;
2326
+ if ( handlers ) {
2327
+ if ( !event ) {
2328
+ event = {};
2329
+ }
2330
+ if ( event.type !== type ) {
2331
+ event.type = type;
2332
+ }
2333
+ // Clone handlers array, so any handlers added/removed do not affect it.
2334
+ handlers = handlers.slice();
2335
+ l = handlers.length;
2336
+ while ( l-- ) {
2337
+ obj = handlers[l];
2338
+ try {
2339
+ if ( obj.handleEvent ) {
2340
+ obj.handleEvent( event );
2341
+ } else {
2342
+ obj.call( this, event );
2343
+ }
2344
+ } catch ( error ) {
2345
+ error.details = 'Squire: fireEvent error. Event type: ' + type;
2346
+ this.didError( error );
2347
+ }
2032
2348
  }
2349
+ }
2350
+ return this;
2351
+ };
2033
2352
 
2034
- // Write out data
2035
- if ( range ) {
2036
- this._saveRangeToBookmark( range );
2353
+ proto.destroy = function () {
2354
+ var win = this._win,
2355
+ doc = this._doc,
2356
+ events = this._events,
2357
+ type;
2358
+ win.removeEventListener( 'focus', this, false );
2359
+ win.removeEventListener( 'blur', this, false );
2360
+ for ( type in events ) {
2361
+ if ( !customEvents[ type ] ) {
2362
+ doc.removeEventListener( type, this, true );
2363
+ }
2364
+ }
2365
+ if ( this._mutation ) {
2366
+ this._mutation.disconnect();
2367
+ }
2368
+ var l = instances.length;
2369
+ while ( l-- ) {
2370
+ if ( instances[l] === this ) {
2371
+ instances.splice( l, 1 );
2037
2372
  }
2038
- undoStack[ undoIndex ] = this._getHTML();
2039
- this._undoStackLength += 1;
2040
- this._isInUndoState = true;
2041
2373
  }
2042
2374
  };
2043
2375
 
2044
- proto.undo = function () {
2045
- // Sanity check: must not be at beginning of the history stack
2046
- if ( this._undoIndex !== 0 || !this._isInUndoState ) {
2047
- // Make sure any changes since last checkpoint are saved.
2048
- this._recordUndoState( this.getSelection() );
2376
+ proto.handleEvent = function ( event ) {
2377
+ this.fireEvent( event.type, event );
2378
+ };
2049
2379
 
2050
- this._undoIndex -= 1;
2051
- this._setHTML( this._undoStack[ this._undoIndex ] );
2052
- var range = this._getRangeAndRemoveBookmark();
2053
- if ( range ) {
2054
- this.setSelection( range );
2055
- }
2056
- this._isInUndoState = true;
2057
- this.fireEvent( 'undoStateChange', {
2058
- canUndo: this._undoIndex !== 0,
2059
- canRedo: true
2380
+ proto.addEventListener = function ( type, fn ) {
2381
+ var handlers = this._events[ type ];
2382
+ if ( !fn ) {
2383
+ this.didError({
2384
+ name: 'Squire: addEventListener with null or undefined fn',
2385
+ message: 'Event type: ' + type
2060
2386
  });
2061
- this.fireEvent( 'input' );
2387
+ return this;
2062
2388
  }
2389
+ if ( !handlers ) {
2390
+ handlers = this._events[ type ] = [];
2391
+ if ( !customEvents[ type ] ) {
2392
+ this._doc.addEventListener( type, this, true );
2393
+ }
2394
+ }
2395
+ handlers.push( fn );
2063
2396
  return this;
2064
2397
  };
2065
2398
 
2066
- proto.redo = function () {
2067
- // Sanity check: must not be at end of stack and must be in an undo
2068
- // state.
2069
- var undoIndex = this._undoIndex,
2070
- undoStackLength = this._undoStackLength;
2071
- if ( undoIndex + 1 < undoStackLength && this._isInUndoState ) {
2072
- this._undoIndex += 1;
2073
- this._setHTML( this._undoStack[ this._undoIndex ] );
2074
- var range = this._getRangeAndRemoveBookmark();
2075
- if ( range ) {
2076
- this.setSelection( range );
2399
+ proto.removeEventListener = function ( type, fn ) {
2400
+ var handlers = this._events[ type ],
2401
+ l;
2402
+ if ( handlers ) {
2403
+ l = handlers.length;
2404
+ while ( l-- ) {
2405
+ if ( handlers[l] === fn ) {
2406
+ handlers.splice( l, 1 );
2407
+ }
2408
+ }
2409
+ if ( !handlers.length ) {
2410
+ delete this._events[ type ];
2411
+ if ( !customEvents[ type ] ) {
2412
+ this._doc.removeEventListener( type, this, false );
2413
+ }
2077
2414
  }
2078
- this.fireEvent( 'undoStateChange', {
2079
- canUndo: true,
2080
- canRedo: undoIndex + 2 < undoStackLength
2081
- });
2082
- this.fireEvent( 'input' );
2083
2415
  }
2084
2416
  return this;
2085
2417
  };
2086
2418
 
2087
- // --- Inline formatting ---
2088
-
2089
- // Looks for matching tag and attributes, so won't work
2090
- // if <strong> instead of <b> etc.
2091
- proto.hasFormat = function ( tag, attributes, range ) {
2092
- // 1. Normalise the arguments and get selection
2093
- tag = tag.toUpperCase();
2094
- if ( !attributes ) { attributes = {}; }
2095
- if ( !range && !( range = this.getSelection() ) ) {
2096
- return false;
2097
- }
2419
+ // --- Selection and Path ---
2098
2420
 
2099
- // If the common ancestor is inside the tag we require, we definitely
2100
- // have the format.
2101
- var root = range.commonAncestorContainer,
2102
- walker, node;
2103
- if ( getNearest( root, tag, attributes ) ) {
2104
- return true;
2421
+ proto._createRange =
2422
+ function ( range, startOffset, endContainer, endOffset ) {
2423
+ if ( range instanceof this._win.Range ) {
2424
+ return range.cloneRange();
2105
2425
  }
2106
-
2107
- // If common ancestor is a text node and doesn't have the format, we
2108
- // definitely don't have it.
2109
- if ( root.nodeType === TEXT_NODE ) {
2110
- return false;
2426
+ var domRange = this._doc.createRange();
2427
+ domRange.setStart( range, startOffset );
2428
+ if ( endContainer ) {
2429
+ domRange.setEnd( endContainer, endOffset );
2430
+ } else {
2431
+ domRange.setEnd( range, startOffset );
2111
2432
  }
2433
+ return domRange;
2434
+ };
2112
2435
 
2113
- // Otherwise, check each text node at least partially contained within
2114
- // the selection and make sure all of them have the format we want.
2115
- walker = new TreeWalker( root, SHOW_TEXT, function ( node ) {
2116
- return isNodeContainedInRange( range, node, true );
2117
- }, false );
2436
+ proto._moveCursorTo = function ( toStart ) {
2437
+ var body = this._body,
2438
+ range = this._createRange( body, toStart ? 0 : body.childNodes.length );
2439
+ moveRangeBoundariesDownTree( range );
2440
+ this.setSelection( range );
2441
+ return this;
2442
+ };
2443
+ proto.moveCursorToStart = function () {
2444
+ return this._moveCursorTo( true );
2445
+ };
2446
+ proto.moveCursorToEnd = function () {
2447
+ return this._moveCursorTo( false );
2448
+ };
2118
2449
 
2119
- var seenNode = false;
2120
- while ( node = walker.nextNode() ) {
2121
- if ( !getNearest( node, tag, attributes ) ) {
2122
- return false;
2450
+ proto.setSelection = function ( range ) {
2451
+ if ( range ) {
2452
+ // iOS bug: if you don't focus the iframe before setting the
2453
+ // selection, you can end up in a state where you type but the input
2454
+ // doesn't get directed into the contenteditable area but is instead
2455
+ // lost in a black hole. Very strange.
2456
+ if ( isIOS ) {
2457
+ this._win.focus();
2458
+ }
2459
+ var sel = this._getWindowSelection();
2460
+ if ( sel ) {
2461
+ sel.removeAllRanges();
2462
+ sel.addRange( range );
2123
2463
  }
2124
- seenNode = true;
2125
2464
  }
2465
+ return this;
2466
+ };
2126
2467
 
2127
- return seenNode;
2468
+ proto._getWindowSelection = function () {
2469
+ return this._win.getSelection() || null;
2128
2470
  };
2129
2471
 
2130
- proto._addFormat = function ( tag, attributes, range ) {
2131
- // If the range is collapsed we simply insert the node by wrapping
2132
- // it round the range and focus it.
2133
- var el, walker, startContainer, endContainer, startOffset, endOffset,
2134
- node, needsFormat;
2135
-
2136
- if ( range.collapsed ) {
2137
- el = fixCursor( this.createElement( tag, attributes ) );
2138
- insertNodeInRange( range, el );
2139
- range.setStart( el.firstChild, el.firstChild.length );
2140
- range.collapse( true );
2472
+ proto.getSelection = function () {
2473
+ var sel = this._getWindowSelection(),
2474
+ selection, startContainer, endContainer;
2475
+ if ( sel && sel.rangeCount ) {
2476
+ selection = sel.getRangeAt( 0 ).cloneRange();
2477
+ startContainer = selection.startContainer;
2478
+ endContainer = selection.endContainer;
2479
+ // FF can return the selection as being inside an <img>. WTF?
2480
+ if ( startContainer && isLeaf( startContainer ) ) {
2481
+ selection.setStartBefore( startContainer );
2482
+ }
2483
+ if ( endContainer && isLeaf( endContainer ) ) {
2484
+ selection.setEndBefore( endContainer );
2485
+ }
2486
+ this._lastSelection = selection;
2487
+ } else {
2488
+ selection = this._lastSelection;
2141
2489
  }
2142
- // Otherwise we find all the textnodes in the range (splitting
2143
- // partially selected nodes) and if they're not already formatted
2144
- // correctly we wrap them in the appropriate tag.
2145
- else {
2146
- // Create an iterator to walk over all the text nodes under this
2147
- // ancestor which are in the range and not already formatted
2148
- // correctly.
2149
- //
2150
- // In Blink/WebKit, empty blocks may have no text nodes, just a <br>.
2151
- // Therefore we wrap this in the tag as well, as this will then cause it
2152
- // to apply when the user types something in the block, which is
2153
- // presumably what was intended.
2490
+ if ( !selection ) {
2491
+ selection = this._createRange( this._body.firstChild, 0 );
2492
+ }
2493
+ return selection;
2494
+ };
2495
+
2496
+ proto.getSelectedText = function () {
2497
+ var range = this.getSelection(),
2154
2498
  walker = new TreeWalker(
2155
2499
  range.commonAncestorContainer,
2156
2500
  SHOW_TEXT|SHOW_ELEMENT,
2157
2501
  function ( node ) {
2158
- return ( node.nodeType === TEXT_NODE ||
2159
- node.nodeName === 'BR' ) &&
2160
- isNodeContainedInRange( range, node, true );
2161
- },
2162
- false
2163
- );
2164
-
2165
- // Start at the beginning node of the range and iterate through
2166
- // all the nodes in the range that need formatting.
2167
- startContainer = range.startContainer;
2168
- startOffset = range.startOffset;
2169
- endContainer = range.endContainer;
2170
- endOffset = range.endOffset;
2171
-
2172
- // Make sure we start with a valid node.
2173
- walker.currentNode = startContainer;
2174
- if ( !walker.filter( startContainer ) ) {
2175
- startContainer = walker.nextNode();
2176
- startOffset = 0;
2177
- }
2502
+ return isNodeContainedInRange( range, node, true );
2503
+ }
2504
+ ),
2505
+ startContainer = range.startContainer,
2506
+ endContainer = range.endContainer,
2507
+ node = walker.currentNode = startContainer,
2508
+ textContent = '',
2509
+ addedTextInBlock = false,
2510
+ value;
2178
2511
 
2179
- // If there are no interesting nodes in the selection, abort
2180
- if ( !startContainer ) {
2181
- return range;
2182
- }
2512
+ if ( !walker.filter( node ) ) {
2513
+ node = walker.nextNode();
2514
+ }
2183
2515
 
2184
- do {
2185
- node = walker.currentNode;
2186
- needsFormat = !getNearest( node, tag, attributes );
2187
- if ( needsFormat ) {
2188
- // <br> can never be a container node, so must have a text node
2189
- // if node == (end|start)Container
2190
- if ( node === endContainer && node.length > endOffset ) {
2191
- node.splitText( endOffset );
2516
+ while ( node ) {
2517
+ if ( node.nodeType === TEXT_NODE ) {
2518
+ value = node.data;
2519
+ if ( value && ( /\S/.test( value ) ) ) {
2520
+ if ( node === endContainer ) {
2521
+ value = value.slice( 0, range.endOffset );
2192
2522
  }
2193
- if ( node === startContainer && startOffset ) {
2194
- node = node.splitText( startOffset );
2195
- if ( endContainer === startContainer ) {
2196
- endContainer = node;
2197
- endOffset -= startOffset;
2198
- }
2199
- startContainer = node;
2200
- startOffset = 0;
2523
+ if ( node === startContainer ) {
2524
+ value = value.slice( range.startOffset );
2201
2525
  }
2202
- el = this.createElement( tag, attributes );
2203
- replaceWith( node, el );
2204
- el.appendChild( node );
2526
+ textContent += value;
2527
+ addedTextInBlock = true;
2205
2528
  }
2206
- } while ( walker.nextNode() );
2529
+ } else if ( node.nodeName === 'BR' ||
2530
+ addedTextInBlock && !isInline( node ) ) {
2531
+ textContent += '\n';
2532
+ addedTextInBlock = false;
2533
+ }
2534
+ node = walker.nextNode();
2535
+ }
2207
2536
 
2208
- // If we don't finish inside a text node, offset may have changed.
2209
- if ( endContainer.nodeType !== TEXT_NODE ) {
2210
- if ( node.nodeType === TEXT_NODE ) {
2211
- endContainer = node;
2212
- endOffset = node.length;
2537
+ return textContent;
2538
+ };
2539
+
2540
+ proto.getPath = function () {
2541
+ return this._path;
2542
+ };
2543
+
2544
+ // --- Workaround for browsers that can't focus empty text nodes ---
2545
+
2546
+ // WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=15256
2547
+
2548
+ var removeZWS = function ( root ) {
2549
+ var walker = new TreeWalker( root, SHOW_TEXT, function () {
2550
+ return true;
2551
+ }, false ),
2552
+ parent, node, index;
2553
+ while ( node = walker.nextNode() ) {
2554
+ while ( ( index = node.data.indexOf( ZWS ) ) > -1 ) {
2555
+ if ( node.length === 1 ) {
2556
+ do {
2557
+ parent = node.parentNode;
2558
+ parent.removeChild( node );
2559
+ node = parent;
2560
+ walker.currentNode = parent;
2561
+ } while ( isInline( node ) && !getLength( node ) );
2562
+ break;
2213
2563
  } else {
2214
- // If <br>, we must have just wrapped it, so it must have only
2215
- // one child
2216
- endContainer = node.parentNode;
2217
- endOffset = 1;
2564
+ node.deleteData( index, 1 );
2218
2565
  }
2219
2566
  }
2567
+ }
2568
+ };
2220
2569
 
2221
- // Now set the selection to as it was before
2222
- range = this._createRange(
2223
- startContainer, startOffset, endContainer, endOffset );
2570
+ proto._didAddZWS = function () {
2571
+ this._hasZWS = true;
2572
+ };
2573
+ proto._removeZWS = function () {
2574
+ if ( !this._hasZWS ) {
2575
+ return;
2224
2576
  }
2225
- return range;
2577
+ removeZWS( this._body );
2578
+ this._hasZWS = false;
2226
2579
  };
2227
2580
 
2228
- proto._removeFormat = function ( tag, attributes, range, partial ) {
2229
- // Add bookmark
2230
- this._saveRangeToBookmark( range );
2581
+ // --- Path change events ---
2231
2582
 
2232
- // We need a node in the selection to break the surrounding
2233
- // formatted text.
2234
- var doc = this._doc,
2235
- fixer;
2236
- if ( range.collapsed ) {
2237
- if ( cantFocusEmptyTextNodes ) {
2238
- fixer = doc.createTextNode( ZWS );
2239
- this._didAddZWS();
2240
- } else {
2241
- fixer = doc.createTextNode( '' );
2583
+ proto._updatePath = function ( range, force ) {
2584
+ var anchor = range.startContainer,
2585
+ focus = range.endContainer,
2586
+ newPath;
2587
+ if ( force || anchor !== this._lastAnchorNode ||
2588
+ focus !== this._lastFocusNode ) {
2589
+ this._lastAnchorNode = anchor;
2590
+ this._lastFocusNode = focus;
2591
+ newPath = ( anchor && focus ) ? ( anchor === focus ) ?
2592
+ getPath( focus ) : '(selection)' : '';
2593
+ if ( this._path !== newPath ) {
2594
+ this._path = newPath;
2595
+ this.fireEvent( 'pathChange', { path: newPath } );
2242
2596
  }
2243
- insertNodeInRange( range, fixer );
2244
2597
  }
2598
+ if ( !range.collapsed ) {
2599
+ this.fireEvent( 'select' );
2600
+ }
2601
+ };
2245
2602
 
2246
- // Find block-level ancestor of selection
2247
- var root = range.commonAncestorContainer;
2248
- while ( isInline( root ) ) {
2249
- root = root.parentNode;
2603
+ proto._updatePathOnEvent = function () {
2604
+ this._updatePath( this.getSelection() );
2605
+ };
2606
+
2607
+ // --- Focus ---
2608
+
2609
+ proto.focus = function () {
2610
+ // FF seems to need the body to be focussed (at least on first load).
2611
+ // Chrome also now needs body to be focussed in order to show the cursor
2612
+ // (otherwise it is focussed, but the cursor doesn't appear).
2613
+ // Opera (Presto-variant) however will lose the selection if you call this!
2614
+ if ( !isPresto ) {
2615
+ this._body.focus();
2250
2616
  }
2617
+ this._win.focus();
2618
+ return this;
2619
+ };
2251
2620
 
2252
- // Find text nodes inside formatTags that are not in selection and
2253
- // add an extra tag with the same formatting.
2254
- var startContainer = range.startContainer,
2255
- startOffset = range.startOffset,
2256
- endContainer = range.endContainer,
2257
- endOffset = range.endOffset,
2258
- toWrap = [],
2259
- examineNode = function ( node, exemplar ) {
2260
- // If the node is completely contained by the range then
2261
- // we're going to remove all formatting so ignore it.
2262
- if ( isNodeContainedInRange( range, node, false ) ) {
2263
- return;
2264
- }
2621
+ proto.blur = function () {
2622
+ // IE will remove the whole browser window from focus if you call
2623
+ // win.blur() or body.blur(), so instead we call top.focus() to focus
2624
+ // the top frame, thus blurring this frame. This works in everything
2625
+ // except FF, so we need to call body.blur() in that as well.
2626
+ if ( isGecko ) {
2627
+ this._body.blur();
2628
+ }
2629
+ top.focus();
2630
+ return this;
2631
+ };
2265
2632
 
2266
- var isText = ( node.nodeType === TEXT_NODE ),
2267
- child, next;
2633
+ // --- Bookmarking ---
2268
2634
 
2269
- // If not at least partially contained, wrap entire contents
2270
- // in a clone of the tag we're removing and we're done.
2271
- if ( !isNodeContainedInRange( range, node, true ) ) {
2272
- // Ignore bookmarks and empty text nodes
2273
- if ( node.nodeName !== 'INPUT' &&
2274
- ( !isText || node.data ) ) {
2275
- toWrap.push([ exemplar, node ]);
2276
- }
2277
- return;
2278
- }
2635
+ var startSelectionId = 'squire-selection-start';
2636
+ var endSelectionId = 'squire-selection-end';
2279
2637
 
2280
- // Split any partially selected text nodes.
2281
- if ( isText ) {
2282
- if ( node === endContainer && endOffset !== node.length ) {
2283
- toWrap.push([ exemplar, node.splitText( endOffset ) ]);
2284
- }
2285
- if ( node === startContainer && startOffset ) {
2286
- node.splitText( startOffset );
2287
- toWrap.push([ exemplar, node ]);
2288
- }
2289
- }
2290
- // If not a text node, recurse onto all children.
2291
- // Beware, the tree may be rewritten with each call
2292
- // to examineNode, hence find the next sibling first.
2293
- else {
2294
- for ( child = node.firstChild; child; child = next ) {
2295
- next = child.nextSibling;
2296
- examineNode( child, exemplar );
2297
- }
2298
- }
2299
- },
2300
- formatTags = Array.prototype.filter.call(
2301
- root.getElementsByTagName( tag ), function ( el ) {
2302
- return isNodeContainedInRange( range, el, true ) &&
2303
- hasTagAttributes( el, tag, attributes );
2304
- }
2305
- );
2306
-
2307
- if ( !partial ) {
2308
- formatTags.forEach( function ( node ) {
2309
- examineNode( node, node );
2310
- });
2311
- }
2638
+ proto._saveRangeToBookmark = function ( range ) {
2639
+ var startNode = this.createElement( 'INPUT', {
2640
+ id: startSelectionId,
2641
+ type: 'hidden'
2642
+ }),
2643
+ endNode = this.createElement( 'INPUT', {
2644
+ id: endSelectionId,
2645
+ type: 'hidden'
2646
+ }),
2647
+ temp;
2312
2648
 
2313
- // Now wrap unselected nodes in the tag
2314
- toWrap.forEach( function ( item ) {
2315
- // [ exemplar, node ] tuple
2316
- var el = item[0].cloneNode( false ),
2317
- node = item[1];
2318
- replaceWith( node, el );
2319
- el.appendChild( node );
2320
- });
2321
- // and remove old formatting tags.
2322
- formatTags.forEach( function ( el ) {
2323
- replaceWith( el, empty( el ) );
2324
- });
2649
+ insertNodeInRange( range, startNode );
2650
+ range.collapse( false );
2651
+ insertNodeInRange( range, endNode );
2325
2652
 
2326
- // Merge adjacent inlines:
2327
- this._getRangeAndRemoveBookmark( range );
2328
- if ( fixer ) {
2329
- range.collapse( false );
2653
+ // In a collapsed range, the start is sometimes inserted after the end!
2654
+ if ( startNode.compareDocumentPosition( endNode ) &
2655
+ DOCUMENT_POSITION_PRECEDING ) {
2656
+ startNode.id = endSelectionId;
2657
+ endNode.id = startSelectionId;
2658
+ temp = startNode;
2659
+ startNode = endNode;
2660
+ endNode = temp;
2330
2661
  }
2331
- var _range = {
2332
- startContainer: range.startContainer,
2333
- startOffset: range.startOffset,
2334
- endContainer: range.endContainer,
2335
- endOffset: range.endOffset
2336
- };
2337
- mergeInlines( root, _range );
2338
- range.setStart( _range.startContainer, _range.startOffset );
2339
- range.setEnd( _range.endContainer, _range.endOffset );
2340
2662
 
2341
- return range;
2663
+ range.setStartAfter( startNode );
2664
+ range.setEndBefore( endNode );
2342
2665
  };
2343
2666
 
2344
- proto.changeFormat = function ( add, remove, range, partial ) {
2345
- // Normalise the arguments and get selection
2346
- if ( !range && !( range = this.getSelection() ) ) {
2347
- return;
2348
- }
2349
-
2350
- // Save undo checkpoint
2351
- this._recordUndoState( range );
2352
- this._getRangeAndRemoveBookmark( range );
2353
-
2354
- if ( remove ) {
2355
- range = this._removeFormat( remove.tag.toUpperCase(),
2356
- remove.attributes || {}, range, partial );
2357
- }
2358
- if ( add ) {
2359
- range = this._addFormat( add.tag.toUpperCase(),
2360
- add.attributes || {}, range );
2361
- }
2362
-
2363
- this.setSelection( range );
2364
- this._updatePath( range, true );
2667
+ proto._getRangeAndRemoveBookmark = function ( range ) {
2668
+ var doc = this._doc,
2669
+ start = doc.getElementById( startSelectionId ),
2670
+ end = doc.getElementById( endSelectionId );
2365
2671
 
2366
- // We're not still in an undo state
2367
- if ( !canObserveMutations ) {
2368
- this._docWasChanged();
2369
- }
2672
+ if ( start && end ) {
2673
+ var startContainer = start.parentNode,
2674
+ endContainer = end.parentNode,
2675
+ collapsed;
2370
2676
 
2371
- return this;
2372
- };
2677
+ var _range = {
2678
+ startContainer: startContainer,
2679
+ endContainer: endContainer,
2680
+ startOffset: indexOf.call( startContainer.childNodes, start ),
2681
+ endOffset: indexOf.call( endContainer.childNodes, end )
2682
+ };
2373
2683
 
2374
- // --- Block formatting ---
2684
+ if ( startContainer === endContainer ) {
2685
+ _range.endOffset -= 1;
2686
+ }
2375
2687
 
2376
- var tagAfterSplit = {
2377
- DT: 'DD',
2378
- DD: 'DT',
2379
- LI: 'LI'
2380
- };
2688
+ detach( start );
2689
+ detach( end );
2381
2690
 
2382
- var splitBlock = function ( self, block, node, offset ) {
2383
- var splitTag = tagAfterSplit[ block.nodeName ],
2384
- splitProperties = null,
2385
- nodeAfterSplit = split( node, offset, block.parentNode ),
2386
- config = self._config;
2691
+ // Merge any text nodes we split
2692
+ mergeInlines( startContainer, _range );
2693
+ if ( startContainer !== endContainer ) {
2694
+ mergeInlines( endContainer, _range );
2695
+ }
2387
2696
 
2388
- if ( !splitTag ) {
2389
- splitTag = config.blockTag;
2390
- splitProperties = config.blockAttributes;
2391
- }
2697
+ if ( !range ) {
2698
+ range = doc.createRange();
2699
+ }
2700
+ range.setStart( _range.startContainer, _range.startOffset );
2701
+ range.setEnd( _range.endContainer, _range.endOffset );
2702
+ collapsed = range.collapsed;
2392
2703
 
2393
- // Make sure the new node is the correct type.
2394
- if ( !hasTagAttributes( nodeAfterSplit, splitTag, splitProperties ) ) {
2395
- block = createElement( nodeAfterSplit.ownerDocument,
2396
- splitTag, splitProperties );
2397
- if ( nodeAfterSplit.dir ) {
2398
- block.dir = nodeAfterSplit.dir;
2704
+ moveRangeBoundariesDownTree( range );
2705
+ if ( collapsed ) {
2706
+ range.collapse( true );
2399
2707
  }
2400
- replaceWith( nodeAfterSplit, block );
2401
- block.appendChild( empty( nodeAfterSplit ) );
2402
- nodeAfterSplit = block;
2403
2708
  }
2404
- return nodeAfterSplit;
2709
+ return range || null;
2405
2710
  };
2406
2711
 
2407
- proto.forEachBlock = function ( fn, mutates, range ) {
2408
- if ( !range && !( range = this.getSelection() ) ) {
2409
- return this;
2410
- }
2712
+ // --- Undo ---
2411
2713
 
2412
- // Save undo checkpoint
2413
- if ( mutates ) {
2414
- this._recordUndoState( range );
2415
- this._getRangeAndRemoveBookmark( range );
2714
+ proto._keyUpDetectChange = function ( event ) {
2715
+ var code = event.keyCode;
2716
+ // Presume document was changed if:
2717
+ // 1. A modifier key (other than shift) wasn't held down
2718
+ // 2. The key pressed is not in range 16<=x<=20 (control keys)
2719
+ // 3. The key pressed is not in range 33<=x<=45 (navigation keys)
2720
+ if ( !event.ctrlKey && !event.metaKey && !event.altKey &&
2721
+ ( code < 16 || code > 20 ) &&
2722
+ ( code < 33 || code > 45 ) ) {
2723
+ this._docWasChanged();
2416
2724
  }
2725
+ };
2417
2726
 
2418
- var start = getStartBlockOfRange( range ),
2419
- end = getEndBlockOfRange( range );
2420
- if ( start && end ) {
2421
- do {
2422
- if ( fn( start ) || start === end ) { break; }
2423
- } while ( start = getNextBlock( start ) );
2727
+ proto._docWasChanged = function () {
2728
+ if ( canObserveMutations && this._ignoreChange ) {
2729
+ this._ignoreChange = false;
2730
+ return;
2731
+ }
2732
+ if ( this._isInUndoState ) {
2733
+ this._isInUndoState = false;
2734
+ this.fireEvent( 'undoStateChange', {
2735
+ canUndo: true,
2736
+ canRedo: false
2737
+ });
2424
2738
  }
2739
+ this.fireEvent( 'input' );
2740
+ };
2425
2741
 
2426
- if ( mutates ) {
2427
- this.setSelection( range );
2742
+ // Leaves bookmark
2743
+ proto._recordUndoState = function ( range ) {
2744
+ // Don't record if we're already in an undo state
2745
+ if ( !this._isInUndoState ) {
2746
+ // Advance pointer to new position
2747
+ var undoIndex = this._undoIndex += 1,
2748
+ undoStack = this._undoStack;
2428
2749
 
2429
- // Path may have changed
2430
- this._updatePath( range, true );
2750
+ // Truncate stack if longer (i.e. if has been previously undone)
2751
+ if ( undoIndex < this._undoStackLength ) {
2752
+ undoStack.length = this._undoStackLength = undoIndex;
2753
+ }
2431
2754
 
2432
- // We're not still in an undo state
2433
- if ( !canObserveMutations ) {
2434
- this._docWasChanged();
2755
+ // Write out data
2756
+ if ( range ) {
2757
+ this._saveRangeToBookmark( range );
2435
2758
  }
2759
+ undoStack[ undoIndex ] = this._getHTML();
2760
+ this._undoStackLength += 1;
2761
+ this._isInUndoState = true;
2436
2762
  }
2437
- return this;
2438
2763
  };
2439
2764
 
2440
- proto.modifyBlocks = function ( modify, range ) {
2441
- if ( !range && !( range = this.getSelection() ) ) {
2442
- return this;
2443
- }
2765
+ proto.undo = function () {
2766
+ // Sanity check: must not be at beginning of the history stack
2767
+ if ( this._undoIndex !== 0 || !this._isInUndoState ) {
2768
+ // Make sure any changes since last checkpoint are saved.
2769
+ this._recordUndoState( this.getSelection() );
2444
2770
 
2445
- // 1. Save undo checkpoint and bookmark selection
2446
- if ( this._isInUndoState ) {
2447
- this._saveRangeToBookmark( range );
2448
- } else {
2449
- this._recordUndoState( range );
2771
+ this._undoIndex -= 1;
2772
+ this._setHTML( this._undoStack[ this._undoIndex ] );
2773
+ var range = this._getRangeAndRemoveBookmark();
2774
+ if ( range ) {
2775
+ this.setSelection( range );
2776
+ }
2777
+ this._isInUndoState = true;
2778
+ this.fireEvent( 'undoStateChange', {
2779
+ canUndo: this._undoIndex !== 0,
2780
+ canRedo: true
2781
+ });
2782
+ this.fireEvent( 'input' );
2450
2783
  }
2784
+ return this;
2785
+ };
2451
2786
 
2452
- // 2. Expand range to block boundaries
2453
- expandRangeToBlockBoundaries( range );
2787
+ proto.redo = function () {
2788
+ // Sanity check: must not be at end of stack and must be in an undo
2789
+ // state.
2790
+ var undoIndex = this._undoIndex,
2791
+ undoStackLength = this._undoStackLength;
2792
+ if ( undoIndex + 1 < undoStackLength && this._isInUndoState ) {
2793
+ this._undoIndex += 1;
2794
+ this._setHTML( this._undoStack[ this._undoIndex ] );
2795
+ var range = this._getRangeAndRemoveBookmark();
2796
+ if ( range ) {
2797
+ this.setSelection( range );
2798
+ }
2799
+ this.fireEvent( 'undoStateChange', {
2800
+ canUndo: true,
2801
+ canRedo: undoIndex + 2 < undoStackLength
2802
+ });
2803
+ this.fireEvent( 'input' );
2804
+ }
2805
+ return this;
2806
+ };
2454
2807
 
2455
- // 3. Remove range.
2456
- var body = this._body,
2457
- frag;
2458
- moveRangeBoundariesUpTree( range, body );
2459
- frag = extractContentsOfRange( range, body );
2808
+ // --- Inline formatting ---
2460
2809
 
2461
- // 4. Modify tree of fragment and reinsert.
2462
- insertNodeInRange( range, modify.call( this, frag ) );
2810
+ // Looks for matching tag and attributes, so won't work
2811
+ // if <strong> instead of <b> etc.
2812
+ proto.hasFormat = function ( tag, attributes, range ) {
2813
+ // 1. Normalise the arguments and get selection
2814
+ tag = tag.toUpperCase();
2815
+ if ( !attributes ) { attributes = {}; }
2816
+ if ( !range && !( range = this.getSelection() ) ) {
2817
+ return false;
2818
+ }
2463
2819
 
2464
- // 5. Merge containers at edges
2465
- if ( range.endOffset < range.endContainer.childNodes.length ) {
2466
- mergeContainers( range.endContainer.childNodes[ range.endOffset ] );
2820
+ // Sanitize range to prevent weird IE artifacts
2821
+ if ( !range.collapsed &&
2822
+ range.startContainer.nodeType === TEXT_NODE &&
2823
+ range.startOffset === range.startContainer.length &&
2824
+ range.startContainer.nextSibling ) {
2825
+ range.setStartBefore( range.startContainer.nextSibling );
2826
+ }
2827
+ if ( !range.collapsed &&
2828
+ range.endContainer.nodeType === TEXT_NODE &&
2829
+ range.endOffset === 0 &&
2830
+ range.endContainer.previousSibling ) {
2831
+ range.setEndAfter( range.endContainer.previousSibling );
2467
2832
  }
2468
- mergeContainers( range.startContainer.childNodes[ range.startOffset ] );
2469
2833
 
2470
- // 6. Restore selection
2471
- this._getRangeAndRemoveBookmark( range );
2472
- this.setSelection( range );
2473
- this._updatePath( range, true );
2834
+ // If the common ancestor is inside the tag we require, we definitely
2835
+ // have the format.
2836
+ var root = range.commonAncestorContainer,
2837
+ walker, node;
2838
+ if ( getNearest( root, tag, attributes ) ) {
2839
+ return true;
2840
+ }
2474
2841
 
2475
- // 7. We're not still in an undo state
2476
- if ( !canObserveMutations ) {
2477
- this._docWasChanged();
2842
+ // If common ancestor is a text node and doesn't have the format, we
2843
+ // definitely don't have it.
2844
+ if ( root.nodeType === TEXT_NODE ) {
2845
+ return false;
2478
2846
  }
2479
2847
 
2480
- return this;
2481
- };
2848
+ // Otherwise, check each text node at least partially contained within
2849
+ // the selection and make sure all of them have the format we want.
2850
+ walker = new TreeWalker( root, SHOW_TEXT, function ( node ) {
2851
+ return isNodeContainedInRange( range, node, true );
2852
+ }, false );
2482
2853
 
2483
- var increaseBlockQuoteLevel = function ( frag ) {
2484
- return this.createElement( 'BLOCKQUOTE',
2485
- this._config.tagAttributes.blockquote, [
2486
- frag
2487
- ]);
2488
- };
2854
+ var seenNode = false;
2855
+ while ( node = walker.nextNode() ) {
2856
+ if ( !getNearest( node, tag, attributes ) ) {
2857
+ return false;
2858
+ }
2859
+ seenNode = true;
2860
+ }
2489
2861
 
2490
- var decreaseBlockQuoteLevel = function ( frag ) {
2491
- var blockquotes = frag.querySelectorAll( 'blockquote' );
2492
- Array.prototype.filter.call( blockquotes, function ( el ) {
2493
- return !getNearest( el.parentNode, 'BLOCKQUOTE' );
2494
- }).forEach( function ( el ) {
2495
- replaceWith( el, empty( el ) );
2496
- });
2497
- return frag;
2862
+ return seenNode;
2498
2863
  };
2499
2864
 
2500
- var removeBlockQuote = function (/* frag */) {
2501
- return this.createDefaultBlock([
2502
- this.createElement( 'INPUT', {
2503
- id: startSelectionId,
2504
- type: 'hidden'
2505
- }),
2506
- this.createElement( 'INPUT', {
2507
- id: endSelectionId,
2508
- type: 'hidden'
2509
- })
2510
- ]);
2511
- };
2865
+ // Extracts the font-family and font-size (if any) of the element
2866
+ // holding the cursor. If there's a selection, returns an empty object.
2867
+ proto.getFontInfo = function ( range ) {
2868
+ var fontInfo = {
2869
+ color: undefined,
2870
+ backgroundColor: undefined,
2871
+ family: undefined,
2872
+ size: undefined
2873
+ };
2874
+ var seenAttributes = 0;
2875
+ var element, style;
2512
2876
 
2513
- var makeList = function ( self, frag, type ) {
2514
- var walker = getBlockWalker( frag ),
2515
- node, tag, prev, newLi,
2516
- tagAttributes = self._config.tagAttributes,
2517
- listAttrs = tagAttributes[ type.toLowerCase() ],
2518
- listItemAttrs = tagAttributes.li;
2877
+ if ( !range && !( range = this.getSelection() ) ) {
2878
+ return fontInfo;
2879
+ }
2519
2880
 
2520
- while ( node = walker.nextNode() ) {
2521
- tag = node.parentNode.nodeName;
2522
- if ( tag !== 'LI' ) {
2523
- newLi = self.createElement( 'LI', listItemAttrs );
2524
- if ( node.dir ) {
2525
- newLi.dir = node.dir;
2881
+ element = range.commonAncestorContainer;
2882
+ if ( range.collapsed || element.nodeType === TEXT_NODE ) {
2883
+ if ( element.nodeType === TEXT_NODE ) {
2884
+ element = element.parentNode;
2885
+ }
2886
+ while ( seenAttributes < 4 && element && ( style = element.style ) ) {
2887
+ if ( !fontInfo.color ) {
2888
+ fontInfo.color = style.color;
2889
+ seenAttributes += 1;
2526
2890
  }
2527
-
2528
- // Have we replaced the previous block with a new <ul>/<ol>?
2529
- if ( ( prev = node.previousSibling ) &&
2530
- prev.nodeName === type ) {
2531
- prev.appendChild( newLi );
2891
+ if ( !fontInfo.backgroundColor ) {
2892
+ fontInfo.backgroundColor = style.backgroundColor;
2893
+ seenAttributes += 1;
2532
2894
  }
2533
- // Otherwise, replace this block with the <ul>/<ol>
2534
- else {
2535
- replaceWith(
2536
- node,
2537
- self.createElement( type, listAttrs, [
2538
- newLi
2539
- ])
2540
- );
2895
+ if ( !fontInfo.family ) {
2896
+ fontInfo.family = style.fontFamily;
2897
+ seenAttributes += 1;
2541
2898
  }
2542
- newLi.appendChild( node );
2543
- } else {
2544
- node = node.parentNode.parentNode;
2545
- tag = node.nodeName;
2546
- if ( tag !== type && ( /^[OU]L$/.test( tag ) ) ) {
2547
- replaceWith( node,
2548
- self.createElement( type, listAttrs, [ empty( node ) ] )
2549
- );
2899
+ if ( !fontInfo.size ) {
2900
+ fontInfo.size = style.fontSize;
2901
+ seenAttributes += 1;
2550
2902
  }
2903
+ element = element.parentNode;
2551
2904
  }
2552
2905
  }
2553
- };
2554
-
2555
- var makeUnorderedList = function ( frag ) {
2556
- makeList( this, frag, 'UL' );
2557
- return frag;
2558
- };
2906
+ return fontInfo;
2907
+ };
2559
2908
 
2560
- var makeOrderedList = function ( frag ) {
2561
- makeList( this, frag, 'OL' );
2562
- return frag;
2563
- };
2909
+ proto._addFormat = function ( tag, attributes, range ) {
2910
+ // If the range is collapsed we simply insert the node by wrapping
2911
+ // it round the range and focus it.
2912
+ var el, walker, startContainer, endContainer, startOffset, endOffset,
2913
+ node, needsFormat;
2564
2914
 
2565
- var removeList = function ( frag ) {
2566
- var lists = frag.querySelectorAll( 'UL, OL' ),
2567
- i, l, ll, list, listFrag, children, child;
2568
- for ( i = 0, l = lists.length; i < l; i += 1 ) {
2569
- list = lists[i];
2570
- listFrag = empty( list );
2571
- children = listFrag.childNodes;
2572
- ll = children.length;
2573
- while ( ll-- ) {
2574
- child = children[ll];
2575
- replaceWith( child, empty( child ) );
2576
- }
2577
- fixContainer( listFrag );
2578
- replaceWith( list, listFrag );
2915
+ if ( range.collapsed ) {
2916
+ el = fixCursor( this.createElement( tag, attributes ) );
2917
+ insertNodeInRange( range, el );
2918
+ range.setStart( el.firstChild, el.firstChild.length );
2919
+ range.collapse( true );
2579
2920
  }
2580
- return frag;
2581
- };
2921
+ // Otherwise we find all the textnodes in the range (splitting
2922
+ // partially selected nodes) and if they're not already formatted
2923
+ // correctly we wrap them in the appropriate tag.
2924
+ else {
2925
+ // Create an iterator to walk over all the text nodes under this
2926
+ // ancestor which are in the range and not already formatted
2927
+ // correctly.
2928
+ //
2929
+ // In Blink/WebKit, empty blocks may have no text nodes, just a <br>.
2930
+ // Therefore we wrap this in the tag as well, as this will then cause it
2931
+ // to apply when the user types something in the block, which is
2932
+ // presumably what was intended.
2933
+ //
2934
+ // IMG tags are included because we may want to create a link around them,
2935
+ // and adding other styles is harmless.
2936
+ walker = new TreeWalker(
2937
+ range.commonAncestorContainer,
2938
+ SHOW_TEXT|SHOW_ELEMENT,
2939
+ function ( node ) {
2940
+ return ( node.nodeType === TEXT_NODE ||
2941
+ node.nodeName === 'BR' ||
2942
+ node.nodeName === 'IMG'
2943
+ ) && isNodeContainedInRange( range, node, true );
2944
+ },
2945
+ false
2946
+ );
2582
2947
 
2583
- var increaseListLevel = function ( frag ) {
2584
- var items = frag.querySelectorAll( 'LI' ),
2585
- i, l, item,
2586
- type, newParent,
2587
- tagAttributes = this._config.tagAttributes,
2588
- listItemAttrs = tagAttributes.li,
2589
- listAttrs;
2590
- for ( i = 0, l = items.length; i < l; i += 1 ) {
2591
- item = items[i];
2592
- if ( !isContainer( item.firstChild ) ) {
2593
- // type => 'UL' or 'OL'
2594
- type = item.parentNode.nodeName;
2595
- newParent = item.previousSibling;
2596
- if ( !newParent || !( newParent = newParent.lastChild ) ||
2597
- newParent.nodeName !== type ) {
2598
- listAttrs = tagAttributes[ type.toLowerCase() ];
2599
- replaceWith(
2600
- item,
2601
- this.createElement( 'LI', listItemAttrs, [
2602
- newParent = this.createElement( type, listAttrs )
2603
- ])
2604
- );
2605
- }
2606
- newParent.appendChild( item );
2948
+ // Start at the beginning node of the range and iterate through
2949
+ // all the nodes in the range that need formatting.
2950
+ startContainer = range.startContainer;
2951
+ startOffset = range.startOffset;
2952
+ endContainer = range.endContainer;
2953
+ endOffset = range.endOffset;
2954
+
2955
+ // Make sure we start with a valid node.
2956
+ walker.currentNode = startContainer;
2957
+ if ( !walker.filter( startContainer ) ) {
2958
+ startContainer = walker.nextNode();
2959
+ startOffset = 0;
2607
2960
  }
2608
- }
2609
- return frag;
2610
- };
2611
2961
 
2612
- var decreaseListLevel = function ( frag ) {
2613
- var items = frag.querySelectorAll( 'LI' );
2614
- Array.prototype.filter.call( items, function ( el ) {
2615
- return !isContainer( el.firstChild );
2616
- }).forEach( function ( item ) {
2617
- var parent = item.parentNode,
2618
- newParent = parent.parentNode,
2619
- first = item.firstChild,
2620
- node = first,
2621
- next;
2622
- if ( item.previousSibling ) {
2623
- parent = split( parent, item, newParent );
2962
+ // If there are no interesting nodes in the selection, abort
2963
+ if ( !startContainer ) {
2964
+ return range;
2624
2965
  }
2625
- while ( node ) {
2626
- next = node.nextSibling;
2627
- if ( isContainer( node ) ) {
2628
- break;
2629
- }
2630
- newParent.insertBefore( node, parent );
2631
- node = next;
2632
- }
2633
- if ( newParent.nodeName === 'LI' && first.previousSibling ) {
2634
- split( newParent, first, newParent.parentNode );
2635
- }
2636
- while ( item !== frag && !item.childNodes.length ) {
2637
- parent = item.parentNode;
2638
- parent.removeChild( item );
2639
- item = parent;
2640
- }
2641
- }, this );
2642
- fixContainer( frag );
2643
- return frag;
2644
- };
2645
2966
 
2646
- // --- Clean ---
2647
-
2648
- var linkRegExp = /\b((?:(?:ht|f)tps?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,}\/)(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\((?:[^\s()<>]+|(?:\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))|([\w\-.%+]+@(?:[\w\-]+\.)+[A-Z]{2,}\b)/i;
2967
+ do {
2968
+ node = walker.currentNode;
2969
+ needsFormat = !getNearest( node, tag, attributes );
2970
+ if ( needsFormat ) {
2971
+ // <br> can never be a container node, so must have a text node
2972
+ // if node == (end|start)Container
2973
+ if ( node === endContainer && node.length > endOffset ) {
2974
+ node.splitText( endOffset );
2975
+ }
2976
+ if ( node === startContainer && startOffset ) {
2977
+ node = node.splitText( startOffset );
2978
+ if ( endContainer === startContainer ) {
2979
+ endContainer = node;
2980
+ endOffset -= startOffset;
2981
+ }
2982
+ startContainer = node;
2983
+ startOffset = 0;
2984
+ }
2985
+ el = this.createElement( tag, attributes );
2986
+ replaceWith( node, el );
2987
+ el.appendChild( node );
2988
+ }
2989
+ } while ( walker.nextNode() );
2649
2990
 
2650
- var addLinks = function ( frag ) {
2651
- var doc = frag.ownerDocument,
2652
- walker = new TreeWalker( frag, SHOW_TEXT,
2653
- function ( node ) {
2654
- return !getNearest( node, 'A' );
2655
- }, false ),
2656
- node, data, parent, match, index, endIndex, child;
2657
- while ( node = walker.nextNode() ) {
2658
- data = node.data;
2659
- parent = node.parentNode;
2660
- while ( match = linkRegExp.exec( data ) ) {
2661
- index = match.index;
2662
- endIndex = index + match[0].length;
2663
- if ( index ) {
2664
- child = doc.createTextNode( data.slice( 0, index ) );
2665
- parent.insertBefore( child, node );
2991
+ // If we don't finish inside a text node, offset may have changed.
2992
+ if ( endContainer.nodeType !== TEXT_NODE ) {
2993
+ if ( node.nodeType === TEXT_NODE ) {
2994
+ endContainer = node;
2995
+ endOffset = node.length;
2996
+ } else {
2997
+ // If <br>, we must have just wrapped it, so it must have only
2998
+ // one child
2999
+ endContainer = node.parentNode;
3000
+ endOffset = 1;
2666
3001
  }
2667
- child = doc.createElement( 'A' );
2668
- child.textContent = data.slice( index, endIndex );
2669
- child.href = match[1] ?
2670
- /^(?:ht|f)tps?:/.test( match[1] ) ?
2671
- match[1] :
2672
- 'http://' + match[1] :
2673
- 'mailto:' + match[2];
2674
- parent.insertBefore( child, node );
2675
- node.data = data = data.slice( endIndex );
2676
3002
  }
3003
+
3004
+ // Now set the selection to as it was before
3005
+ range = this._createRange(
3006
+ startContainer, startOffset, endContainer, endOffset );
2677
3007
  }
3008
+ return range;
2678
3009
  };
2679
3010
 
2680
- var allowedBlock = /^(?:A(?:DDRESS|RTICLE|SIDE|UDIO)|BLOCKQUOTE|CAPTION|D(?:[DLT]|IV)|F(?:IGURE|OOTER)|H[1-6]|HEADER|L(?:ABEL|EGEND|I)|O(?:L|UTPUT)|P(?:RE)?|SECTION|T(?:ABLE|BODY|D|FOOT|H|HEAD|R)|UL)$/;
2681
-
2682
- var fontSizes = {
2683
- 1: 10,
2684
- 2: 13,
2685
- 3: 16,
2686
- 4: 18,
2687
- 5: 24,
2688
- 6: 32,
2689
- 7: 48
2690
- };
3011
+ proto._removeFormat = function ( tag, attributes, range, partial ) {
3012
+ // Add bookmark
3013
+ this._saveRangeToBookmark( range );
2691
3014
 
2692
- var spanToSemantic = {
2693
- backgroundColor: {
2694
- regexp: notWS,
2695
- replace: function ( doc, colour ) {
2696
- return createElement( doc, 'SPAN', {
2697
- 'class': 'highlight',
2698
- style: 'background-color: ' + colour
2699
- });
2700
- }
2701
- },
2702
- color: {
2703
- regexp: notWS,
2704
- replace: function ( doc, colour ) {
2705
- return createElement( doc, 'SPAN', {
2706
- 'class': 'colour',
2707
- style: 'color:' + colour
2708
- });
2709
- }
2710
- },
2711
- fontWeight: {
2712
- regexp: /^bold/i,
2713
- replace: function ( doc ) {
2714
- return createElement( doc, 'B' );
2715
- }
2716
- },
2717
- fontStyle: {
2718
- regexp: /^italic/i,
2719
- replace: function ( doc ) {
2720
- return createElement( doc, 'I' );
2721
- }
2722
- },
2723
- fontFamily: {
2724
- regexp: notWS,
2725
- replace: function ( doc, family ) {
2726
- return createElement( doc, 'SPAN', {
2727
- 'class': 'font',
2728
- style: 'font-family:' + family
2729
- });
2730
- }
2731
- },
2732
- fontSize: {
2733
- regexp: notWS,
2734
- replace: function ( doc, size ) {
2735
- return createElement( doc, 'SPAN', {
2736
- 'class': 'size',
2737
- style: 'font-size:' + size
2738
- });
3015
+ // We need a node in the selection to break the surrounding
3016
+ // formatted text.
3017
+ var doc = this._doc,
3018
+ fixer;
3019
+ if ( range.collapsed ) {
3020
+ if ( cantFocusEmptyTextNodes ) {
3021
+ fixer = doc.createTextNode( ZWS );
3022
+ this._didAddZWS();
3023
+ } else {
3024
+ fixer = doc.createTextNode( '' );
2739
3025
  }
3026
+ insertNodeInRange( range, fixer );
2740
3027
  }
2741
- };
2742
3028
 
2743
- var replaceWithTag = function ( tag ) {
2744
- return function ( node, parent ) {
2745
- var el = createElement( node.ownerDocument, tag );
2746
- parent.replaceChild( el, node );
2747
- el.appendChild( empty( node ) );
2748
- return el;
2749
- };
2750
- };
2751
-
2752
- var stylesRewriters = {
2753
- SPAN: function ( span, parent ) {
2754
- var style = span.style,
2755
- doc = span.ownerDocument,
2756
- attr, converter, css, newTreeBottom, newTreeTop, el;
3029
+ // Find block-level ancestor of selection
3030
+ var root = range.commonAncestorContainer;
3031
+ while ( isInline( root ) ) {
3032
+ root = root.parentNode;
3033
+ }
2757
3034
 
2758
- for ( attr in spanToSemantic ) {
2759
- converter = spanToSemantic[ attr ];
2760
- css = style[ attr ];
2761
- if ( css && converter.regexp.test( css ) ) {
2762
- el = converter.replace( doc, css );
2763
- if ( newTreeBottom ) {
2764
- newTreeBottom.appendChild( el );
2765
- }
2766
- newTreeBottom = el;
2767
- if ( !newTreeTop ) {
2768
- newTreeTop = el;
2769
- }
3035
+ // Find text nodes inside formatTags that are not in selection and
3036
+ // add an extra tag with the same formatting.
3037
+ var startContainer = range.startContainer,
3038
+ startOffset = range.startOffset,
3039
+ endContainer = range.endContainer,
3040
+ endOffset = range.endOffset,
3041
+ toWrap = [],
3042
+ examineNode = function ( node, exemplar ) {
3043
+ // If the node is completely contained by the range then
3044
+ // we're going to remove all formatting so ignore it.
3045
+ if ( isNodeContainedInRange( range, node, false ) ) {
3046
+ return;
2770
3047
  }
2771
- }
2772
3048
 
2773
- if ( newTreeTop ) {
2774
- newTreeBottom.appendChild( empty( span ) );
2775
- parent.replaceChild( newTreeTop, span );
2776
- }
3049
+ var isText = ( node.nodeType === TEXT_NODE ),
3050
+ child, next;
2777
3051
 
2778
- return newTreeBottom || span;
2779
- },
2780
- STRONG: replaceWithTag( 'B' ),
2781
- EM: replaceWithTag( 'I' ),
2782
- STRIKE: replaceWithTag( 'S' ),
2783
- FONT: function ( node, parent ) {
2784
- var face = node.face,
2785
- size = node.size,
2786
- colour = node.color,
2787
- doc = node.ownerDocument,
2788
- fontSpan, sizeSpan, colourSpan,
2789
- newTreeBottom, newTreeTop;
2790
- if ( face ) {
2791
- fontSpan = createElement( doc, 'SPAN', {
2792
- 'class': 'font',
2793
- style: 'font-family:' + face
2794
- });
2795
- newTreeTop = fontSpan;
2796
- newTreeBottom = fontSpan;
2797
- }
2798
- if ( size ) {
2799
- sizeSpan = createElement( doc, 'SPAN', {
2800
- 'class': 'size',
2801
- style: 'font-size:' + fontSizes[ size ] + 'px'
2802
- });
2803
- if ( !newTreeTop ) {
2804
- newTreeTop = sizeSpan;
2805
- }
2806
- if ( newTreeBottom ) {
2807
- newTreeBottom.appendChild( sizeSpan );
3052
+ // If not at least partially contained, wrap entire contents
3053
+ // in a clone of the tag we're removing and we're done.
3054
+ if ( !isNodeContainedInRange( range, node, true ) ) {
3055
+ // Ignore bookmarks and empty text nodes
3056
+ if ( node.nodeName !== 'INPUT' &&
3057
+ ( !isText || node.data ) ) {
3058
+ toWrap.push([ exemplar, node ]);
3059
+ }
3060
+ return;
2808
3061
  }
2809
- newTreeBottom = sizeSpan;
2810
- }
2811
- if ( colour && /^#?([\dA-F]{3}){1,2}$/i.test( colour ) ) {
2812
- if ( colour.charAt( 0 ) !== '#' ) {
2813
- colour = '#' + colour;
3062
+
3063
+ // Split any partially selected text nodes.
3064
+ if ( isText ) {
3065
+ if ( node === endContainer && endOffset !== node.length ) {
3066
+ toWrap.push([ exemplar, node.splitText( endOffset ) ]);
3067
+ }
3068
+ if ( node === startContainer && startOffset ) {
3069
+ node.splitText( startOffset );
3070
+ toWrap.push([ exemplar, node ]);
3071
+ }
2814
3072
  }
2815
- colourSpan = createElement( doc, 'SPAN', {
2816
- 'class': 'colour',
2817
- style: 'color:' + colour
2818
- });
2819
- if ( !newTreeTop ) {
2820
- newTreeTop = colourSpan;
3073
+ // If not a text node, recurse onto all children.
3074
+ // Beware, the tree may be rewritten with each call
3075
+ // to examineNode, hence find the next sibling first.
3076
+ else {
3077
+ for ( child = node.firstChild; child; child = next ) {
3078
+ next = child.nextSibling;
3079
+ examineNode( child, exemplar );
3080
+ }
2821
3081
  }
2822
- if ( newTreeBottom ) {
2823
- newTreeBottom.appendChild( colourSpan );
3082
+ },
3083
+ formatTags = Array.prototype.filter.call(
3084
+ root.getElementsByTagName( tag ), function ( el ) {
3085
+ return isNodeContainedInRange( range, el, true ) &&
3086
+ hasTagAttributes( el, tag, attributes );
2824
3087
  }
2825
- newTreeBottom = colourSpan;
2826
- }
2827
- if ( !newTreeTop ) {
2828
- newTreeTop = newTreeBottom = createElement( doc, 'SPAN' );
2829
- }
2830
- parent.replaceChild( newTreeTop, node );
2831
- newTreeBottom.appendChild( empty( node ) );
2832
- return newTreeBottom;
2833
- },
2834
- TT: function ( node, parent ) {
2835
- var el = createElement( node.ownerDocument, 'SPAN', {
2836
- 'class': 'font',
2837
- style: 'font-family:menlo,consolas,"courier new",monospace'
3088
+ );
3089
+
3090
+ if ( !partial ) {
3091
+ formatTags.forEach( function ( node ) {
3092
+ examineNode( node, node );
2838
3093
  });
2839
- parent.replaceChild( el, node );
2840
- el.appendChild( empty( node ) );
2841
- return el;
2842
3094
  }
2843
- };
2844
3095
 
2845
- var removeEmptyInlines = function ( root ) {
2846
- var children = root.childNodes,
2847
- l = children.length,
2848
- child;
2849
- while ( l-- ) {
2850
- child = children[l];
2851
- if ( child.nodeType === ELEMENT_NODE && !isLeaf( child ) ) {
2852
- removeEmptyInlines( child );
2853
- if ( isInline( child ) && !child.firstChild ) {
2854
- root.removeChild( child );
2855
- }
2856
- } else if ( child.nodeType === TEXT_NODE && !child.data ) {
2857
- root.removeChild( child );
2858
- }
3096
+ // Now wrap unselected nodes in the tag
3097
+ toWrap.forEach( function ( item ) {
3098
+ // [ exemplar, node ] tuple
3099
+ var el = item[0].cloneNode( false ),
3100
+ node = item[1];
3101
+ replaceWith( node, el );
3102
+ el.appendChild( node );
3103
+ });
3104
+ // and remove old formatting tags.
3105
+ formatTags.forEach( function ( el ) {
3106
+ replaceWith( el, empty( el ) );
3107
+ });
3108
+
3109
+ // Merge adjacent inlines:
3110
+ this._getRangeAndRemoveBookmark( range );
3111
+ if ( fixer ) {
3112
+ range.collapse( false );
2859
3113
  }
3114
+ var _range = {
3115
+ startContainer: range.startContainer,
3116
+ startOffset: range.startOffset,
3117
+ endContainer: range.endContainer,
3118
+ endOffset: range.endOffset
3119
+ };
3120
+ mergeInlines( root, _range );
3121
+ range.setStart( _range.startContainer, _range.startOffset );
3122
+ range.setEnd( _range.endContainer, _range.endOffset );
3123
+
3124
+ return range;
2860
3125
  };
2861
3126
 
2862
- /*
2863
- Two purposes:
3127
+ proto.changeFormat = function ( add, remove, range, partial ) {
3128
+ // Normalise the arguments and get selection
3129
+ if ( !range && !( range = this.getSelection() ) ) {
3130
+ return;
3131
+ }
2864
3132
 
2865
- 1. Remove nodes we don't want, such as weird <o:p> tags, comment nodes
2866
- and whitespace nodes.
2867
- 2. Convert inline tags into our preferred format.
2868
- */
2869
- var cleanTree = function ( node, allowStyles ) {
2870
- var children = node.childNodes,
2871
- i, l, child, nodeName, nodeType, rewriter, childLength,
2872
- data, j, ll;
2873
- for ( i = 0, l = children.length; i < l; i += 1 ) {
2874
- child = children[i];
2875
- nodeName = child.nodeName;
2876
- nodeType = child.nodeType;
2877
- rewriter = stylesRewriters[ nodeName ];
2878
- if ( nodeType === ELEMENT_NODE ) {
2879
- childLength = child.childNodes.length;
2880
- if ( rewriter ) {
2881
- child = rewriter( child, node );
2882
- } else if ( !allowedBlock.test( nodeName ) &&
2883
- !isInline( child ) ) {
2884
- i -= 1;
2885
- l += childLength - 1;
2886
- node.replaceChild( empty( child ), child );
2887
- continue;
2888
- } else if ( !allowStyles && child.style.cssText ) {
2889
- child.removeAttribute( 'style' );
2890
- }
2891
- if ( childLength ) {
2892
- cleanTree( child, allowStyles );
2893
- }
2894
- } else {
2895
- if ( nodeType === TEXT_NODE ) {
2896
- data = child.data;
2897
- // Use \S instead of notWS, because we want to remove nodes
2898
- // which are just nbsp, in order to cleanup <div>nbsp<br></div>
2899
- // construct.
2900
- if ( /\S/.test( data ) ) {
2901
- // If the parent node is inline, don't trim this node as
2902
- // it probably isn't at the end of the block.
2903
- if ( isInline( node ) ) {
2904
- continue;
2905
- }
2906
- j = 0;
2907
- ll = data.length;
2908
- if ( !i || !isInline( children[ i - 1 ] ) ) {
2909
- while ( j < ll && !notWS.test( data.charAt( j ) ) ) {
2910
- j += 1;
2911
- }
2912
- if ( j ) {
2913
- child.data = data = data.slice( j );
2914
- ll -= j;
2915
- }
2916
- }
2917
- if ( i + 1 === l || !isInline( children[ i + 1 ] ) ) {
2918
- j = ll;
2919
- while ( j > 0 && !notWS.test( data.charAt( j - 1 ) ) ) {
2920
- j -= 1;
2921
- }
2922
- if ( j < ll ) {
2923
- child.data = data.slice( 0, j );
2924
- }
2925
- }
2926
- continue;
2927
- }
2928
- // If we have just white space, it may still be important if it
2929
- // separates two inline nodes, e.g. "<a>link</a> <a>link</a>".
2930
- else if ( i && i + 1 < l &&
2931
- isInline( children[ i - 1 ] ) &&
2932
- isInline( children[ i + 1 ] ) ) {
2933
- child.data = ' ';
2934
- continue;
2935
- }
2936
- }
2937
- node.removeChild( child );
2938
- i -= 1;
2939
- l -= 1;
2940
- }
3133
+ // Save undo checkpoint
3134
+ this._recordUndoState( range );
3135
+ this._getRangeAndRemoveBookmark( range );
3136
+
3137
+ if ( remove ) {
3138
+ range = this._removeFormat( remove.tag.toUpperCase(),
3139
+ remove.attributes || {}, range, partial );
3140
+ }
3141
+ if ( add ) {
3142
+ range = this._addFormat( add.tag.toUpperCase(),
3143
+ add.attributes || {}, range );
3144
+ }
3145
+
3146
+ this.setSelection( range );
3147
+ this._updatePath( range, true );
3148
+
3149
+ // We're not still in an undo state
3150
+ if ( !canObserveMutations ) {
3151
+ this._docWasChanged();
2941
3152
  }
2942
- return node;
2943
- };
2944
3153
 
2945
- var notWSTextNode = function ( node ) {
2946
- return node.nodeType === ELEMENT_NODE ?
2947
- node.nodeName === 'BR' :
2948
- notWS.test( node.data );
3154
+ return this;
2949
3155
  };
2950
- var isLineBreak = function ( br ) {
2951
- var block = br.parentNode,
2952
- walker;
2953
- while ( isInline( block ) ) {
2954
- block = block.parentNode;
2955
- }
2956
- walker = new TreeWalker(
2957
- block, SHOW_ELEMENT|SHOW_TEXT, notWSTextNode );
2958
- walker.currentNode = br;
2959
- return !!walker.nextNode();
3156
+
3157
+ // --- Block formatting ---
3158
+
3159
+ var tagAfterSplit = {
3160
+ DT: 'DD',
3161
+ DD: 'DT',
3162
+ LI: 'LI'
2960
3163
  };
2961
3164
 
2962
- // <br> elements are treated specially, and differently depending on the
2963
- // browser, when in rich text editor mode. When adding HTML from external
2964
- // sources, we must remove them, replacing the ones that actually affect
2965
- // line breaks with a split of the block element containing it (and wrapping
2966
- // any not inside a block). Browsers that want <br> elements at the end of
2967
- // each block will then have them added back in a later fixCursor method
2968
- // call.
2969
- var cleanupBRs = function ( root ) {
2970
- var brs = root.querySelectorAll( 'BR' ),
2971
- brBreaksLine = [],
2972
- l = brs.length,
2973
- i, br, block;
3165
+ var splitBlock = function ( self, block, node, offset ) {
3166
+ var splitTag = tagAfterSplit[ block.nodeName ],
3167
+ splitProperties = null,
3168
+ nodeAfterSplit = split( node, offset, block.parentNode ),
3169
+ config = self._config;
2974
3170
 
2975
- // Must calculate whether the <br> breaks a line first, because if we
2976
- // have two <br>s next to each other, after the first one is converted
2977
- // to a block split, the second will be at the end of a block and
2978
- // therefore seem to not be a line break. But in its original context it
2979
- // was, so we should also convert it to a block split.
2980
- for ( i = 0; i < l; i += 1 ) {
2981
- brBreaksLine[i] = isLineBreak( brs[i] );
3171
+ if ( !splitTag ) {
3172
+ splitTag = config.blockTag;
3173
+ splitProperties = config.blockAttributes;
2982
3174
  }
2983
- while ( l-- ) {
2984
- br = brs[l];
2985
- // Cleanup may have removed it
2986
- block = br.parentNode;
2987
- if ( !block ) { continue; }
2988
- while ( isInline( block ) ) {
2989
- block = block.parentNode;
2990
- }
2991
- // If this is not inside a block, replace it by wrapping
2992
- // inlines in a <div>.
2993
- if ( !isBlock( block ) ) {
2994
- fixContainer( block );
2995
- }
2996
- else {
2997
- // If it doesn't break a line, just remove it; it's not doing
2998
- // anything useful. We'll add it back later if required by the
2999
- // browser. If it breaks a line, split the block or leave it as
3000
- // appropriate.
3001
- if ( brBreaksLine[l] ) {
3002
- // If in a <div>, split, but anywhere else we might change
3003
- // the formatting too much (e.g. <li> -> to two list items!)
3004
- // so just play it safe and leave it.
3005
- if ( block.nodeName !== 'DIV' ) {
3006
- continue;
3007
- }
3008
- split( br.parentNode, br, block.parentNode );
3009
- }
3010
- detach( br );
3175
+
3176
+ // Make sure the new node is the correct type.
3177
+ if ( !hasTagAttributes( nodeAfterSplit, splitTag, splitProperties ) ) {
3178
+ block = createElement( nodeAfterSplit.ownerDocument,
3179
+ splitTag, splitProperties );
3180
+ if ( nodeAfterSplit.dir ) {
3181
+ block.dir = nodeAfterSplit.dir;
3011
3182
  }
3183
+ replaceWith( nodeAfterSplit, block );
3184
+ block.appendChild( empty( nodeAfterSplit ) );
3185
+ nodeAfterSplit = block;
3012
3186
  }
3187
+ return nodeAfterSplit;
3013
3188
  };
3014
3189
 
3015
- proto._ensureBottomLine = function () {
3016
- var body = this._body,
3017
- last = body.lastElementChild;
3018
- if ( !last ||
3019
- last.nodeName !== this._config.blockTag || !isBlock( last ) ) {
3020
- body.appendChild( this.createDefaultBlock() );
3190
+ proto.forEachBlock = function ( fn, mutates, range ) {
3191
+ if ( !range && !( range = this.getSelection() ) ) {
3192
+ return this;
3021
3193
  }
3022
- };
3023
3194
 
3024
- // --- Cut and Paste ---
3025
-
3026
- proto._onCut = function () {
3027
3195
  // Save undo checkpoint
3028
- var range = this.getSelection();
3029
- var self = this;
3030
- this._recordUndoState( range );
3031
- this._getRangeAndRemoveBookmark( range );
3032
- this.setSelection( range );
3033
- setTimeout( function () {
3034
- try {
3035
- // If all content removed, ensure div at start of body.
3036
- self._ensureBottomLine();
3037
- } catch ( error ) {
3038
- self.didError( error );
3196
+ if ( mutates ) {
3197
+ this._recordUndoState( range );
3198
+ this._getRangeAndRemoveBookmark( range );
3199
+ }
3200
+
3201
+ var start = getStartBlockOfRange( range ),
3202
+ end = getEndBlockOfRange( range );
3203
+ if ( start && end ) {
3204
+ do {
3205
+ if ( fn( start ) || start === end ) { break; }
3206
+ } while ( start = getNextBlock( start ) );
3207
+ }
3208
+
3209
+ if ( mutates ) {
3210
+ this.setSelection( range );
3211
+
3212
+ // Path may have changed
3213
+ this._updatePath( range, true );
3214
+
3215
+ // We're not still in an undo state
3216
+ if ( !canObserveMutations ) {
3217
+ this._docWasChanged();
3039
3218
  }
3040
- }, 0 );
3219
+ }
3220
+ return this;
3041
3221
  };
3042
3222
 
3043
- proto._onPaste = function ( event ) {
3044
- if ( this._awaitingPaste ) { return; }
3223
+ proto.modifyBlocks = function ( modify, range ) {
3224
+ if ( !range && !( range = this.getSelection() ) ) {
3225
+ return this;
3226
+ }
3045
3227
 
3046
- // Treat image paste as a drop of an image file.
3047
- var clipboardData = event.clipboardData,
3048
- items = clipboardData && clipboardData.items,
3049
- fireDrop = false,
3050
- hasImage = false,
3051
- l, type;
3052
- if ( items ) {
3053
- l = items.length;
3054
- while ( l-- ) {
3055
- type = items[l].type;
3056
- if ( type === 'text/html' ) {
3057
- hasImage = false;
3058
- break;
3059
- }
3060
- if ( /^image\/.*/.test( type ) ) {
3061
- hasImage = true;
3062
- }
3063
- }
3064
- if ( hasImage ) {
3065
- event.preventDefault();
3066
- this.fireEvent( 'dragover', {
3067
- dataTransfer: clipboardData,
3068
- /*jshint loopfunc: true */
3069
- preventDefault: function () {
3070
- fireDrop = true;
3071
- }
3072
- /*jshint loopfunc: false */
3073
- });
3074
- if ( fireDrop ) {
3075
- this.fireEvent( 'drop', {
3076
- dataTransfer: clipboardData
3077
- });
3078
- }
3079
- return;
3080
- }
3228
+ // 1. Save undo checkpoint and bookmark selection
3229
+ if ( this._isInUndoState ) {
3230
+ this._saveRangeToBookmark( range );
3231
+ } else {
3232
+ this._recordUndoState( range );
3081
3233
  }
3082
3234
 
3083
- this._awaitingPaste = true;
3235
+ // 2. Expand range to block boundaries
3236
+ expandRangeToBlockBoundaries( range );
3084
3237
 
3085
- var self = this,
3086
- body = this._body,
3087
- range = this.getSelection(),
3088
- startContainer, startOffset, endContainer, endOffset, startBlock;
3238
+ // 3. Remove range.
3239
+ var body = this._body,
3240
+ frag;
3241
+ moveRangeBoundariesUpTree( range, body );
3242
+ frag = extractContentsOfRange( range, body );
3089
3243
 
3090
- // Record undo checkpoint
3091
- self._recordUndoState( range );
3092
- self._getRangeAndRemoveBookmark( range );
3244
+ // 4. Modify tree of fragment and reinsert.
3245
+ insertNodeInRange( range, modify.call( this, frag ) );
3093
3246
 
3094
- // Note current selection. We must do this AFTER recording the undo
3095
- // checkpoint, as this modifies the DOM.
3096
- startContainer = range.startContainer;
3097
- startOffset = range.startOffset;
3098
- endContainer = range.endContainer;
3099
- endOffset = range.endOffset;
3100
- startBlock = getStartBlockOfRange( range );
3247
+ // 5. Merge containers at edges
3248
+ if ( range.endOffset < range.endContainer.childNodes.length ) {
3249
+ mergeContainers( range.endContainer.childNodes[ range.endOffset ] );
3250
+ }
3251
+ mergeContainers( range.startContainer.childNodes[ range.startOffset ] );
3101
3252
 
3102
- // We need to position the pasteArea in the visible portion of the screen
3103
- // to stop the browser auto-scrolling.
3104
- var pasteArea = this.createElement( 'DIV', {
3105
- style: 'position: absolute; overflow: hidden; top:' +
3106
- ( body.scrollTop +
3107
- ( startBlock ? startBlock.getBoundingClientRect().top : 0 ) ) +
3108
- 'px; left: 0; width: 1px; height: 1px;'
3109
- });
3110
- body.appendChild( pasteArea );
3111
- range.selectNodeContents( pasteArea );
3253
+ // 6. Restore selection
3254
+ this._getRangeAndRemoveBookmark( range );
3112
3255
  this.setSelection( range );
3256
+ this._updatePath( range, true );
3113
3257
 
3114
- // A setTimeout of 0 means this is added to the back of the
3115
- // single javascript thread, so it will be executed after the
3116
- // paste event.
3117
- setTimeout( function () {
3118
- try {
3119
- // Get the pasted content and clean
3120
- var frag = empty( detach( pasteArea ) ),
3121
- first = frag.firstChild,
3122
- range = self._createRange(
3123
- startContainer, startOffset, endContainer, endOffset );
3258
+ // 7. We're not still in an undo state
3259
+ if ( !canObserveMutations ) {
3260
+ this._docWasChanged();
3261
+ }
3124
3262
 
3125
- // Was anything actually pasted?
3126
- if ( first ) {
3127
- // Safari and IE like putting extra divs around things.
3128
- if ( first === frag.lastChild &&
3129
- first.nodeName === 'DIV' ) {
3130
- frag.replaceChild( empty( first ), first );
3131
- }
3263
+ return this;
3264
+ };
3132
3265
 
3133
- frag.normalize();
3134
- addLinks( frag );
3135
- cleanTree( frag, false );
3136
- cleanupBRs( frag );
3137
- removeEmptyInlines( frag );
3138
-
3139
- var node = frag,
3140
- doPaste = true,
3141
- event = {
3142
- fragment: frag,
3143
- preventDefault: function () {
3144
- doPaste = false;
3145
- },
3146
- isDefaultPrevented: function () {
3147
- return !doPaste;
3148
- }
3149
- };
3150
- while ( node = getNextBlock( node ) ) {
3151
- fixCursor( node );
3152
- }
3266
+ var increaseBlockQuoteLevel = function ( frag ) {
3267
+ return this.createElement( 'BLOCKQUOTE',
3268
+ this._config.tagAttributes.blockquote, [
3269
+ frag
3270
+ ]);
3271
+ };
3153
3272
 
3154
- self.fireEvent( 'willPaste', event );
3273
+ var decreaseBlockQuoteLevel = function ( frag ) {
3274
+ var blockquotes = frag.querySelectorAll( 'blockquote' );
3275
+ Array.prototype.filter.call( blockquotes, function ( el ) {
3276
+ return !getNearest( el.parentNode, 'BLOCKQUOTE' );
3277
+ }).forEach( function ( el ) {
3278
+ replaceWith( el, empty( el ) );
3279
+ });
3280
+ return frag;
3281
+ };
3155
3282
 
3156
- // Insert pasted data
3157
- if ( doPaste ) {
3158
- insertTreeFragmentIntoRange( range, event.fragment );
3159
- if ( !canObserveMutations ) {
3160
- self._docWasChanged();
3161
- }
3162
- range.collapse( false );
3163
- self._ensureBottomLine();
3164
- }
3283
+ var removeBlockQuote = function (/* frag */) {
3284
+ return this.createDefaultBlock([
3285
+ this.createElement( 'INPUT', {
3286
+ id: startSelectionId,
3287
+ type: 'hidden'
3288
+ }),
3289
+ this.createElement( 'INPUT', {
3290
+ id: endSelectionId,
3291
+ type: 'hidden'
3292
+ })
3293
+ ]);
3294
+ };
3295
+
3296
+ var makeList = function ( self, frag, type ) {
3297
+ var walker = getBlockWalker( frag ),
3298
+ node, tag, prev, newLi,
3299
+ tagAttributes = self._config.tagAttributes,
3300
+ listAttrs = tagAttributes[ type.toLowerCase() ],
3301
+ listItemAttrs = tagAttributes.li;
3302
+
3303
+ while ( node = walker.nextNode() ) {
3304
+ tag = node.parentNode.nodeName;
3305
+ if ( tag !== 'LI' ) {
3306
+ newLi = self.createElement( 'LI', listItemAttrs );
3307
+ if ( node.dir ) {
3308
+ newLi.dir = node.dir;
3309
+ }
3310
+
3311
+ // Have we replaced the previous block with a new <ul>/<ol>?
3312
+ if ( ( prev = node.previousSibling ) &&
3313
+ prev.nodeName === type ) {
3314
+ prev.appendChild( newLi );
3315
+ }
3316
+ // Otherwise, replace this block with the <ul>/<ol>
3317
+ else {
3318
+ replaceWith(
3319
+ node,
3320
+ self.createElement( type, listAttrs, [
3321
+ newLi
3322
+ ])
3323
+ );
3324
+ }
3325
+ newLi.appendChild( node );
3326
+ } else {
3327
+ node = node.parentNode.parentNode;
3328
+ tag = node.nodeName;
3329
+ if ( tag !== type && ( /^[OU]L$/.test( tag ) ) ) {
3330
+ replaceWith( node,
3331
+ self.createElement( type, listAttrs, [ empty( node ) ] )
3332
+ );
3165
3333
  }
3166
-
3167
- self.setSelection( range );
3168
- self._updatePath( range, true );
3169
-
3170
- self._awaitingPaste = false;
3171
- } catch ( error ) {
3172
- self.didError( error );
3173
3334
  }
3174
- }, 0 );
3335
+ }
3175
3336
  };
3176
3337
 
3177
- // --- Keyboard interaction ---
3178
-
3179
- var keys = {
3180
- 8: 'backspace',
3181
- 9: 'tab',
3182
- 13: 'enter',
3183
- 32: 'space',
3184
- 37: 'left',
3185
- 39: 'right',
3186
- 46: 'delete',
3187
- 219: '[',
3188
- 221: ']'
3338
+ var makeUnorderedList = function ( frag ) {
3339
+ makeList( this, frag, 'UL' );
3340
+ return frag;
3189
3341
  };
3190
3342
 
3191
- // Ref: http://unixpapa.com/js/key.html
3192
- proto._onKey = function ( event ) {
3193
- var code = event.keyCode,
3194
- key = keys[ code ],
3195
- modifiers = '',
3196
- range = this.getSelection();
3343
+ var makeOrderedList = function ( frag ) {
3344
+ makeList( this, frag, 'OL' );
3345
+ return frag;
3346
+ };
3197
3347
 
3198
- if ( !key ) {
3199
- key = String.fromCharCode( code ).toLowerCase();
3200
- // Only reliable for letters and numbers
3201
- if ( !/^[A-Za-z0-9]$/.test( key ) ) {
3202
- key = '';
3348
+ var removeList = function ( frag ) {
3349
+ var lists = frag.querySelectorAll( 'UL, OL' ),
3350
+ i, l, ll, list, listFrag, children, child;
3351
+ for ( i = 0, l = lists.length; i < l; i += 1 ) {
3352
+ list = lists[i];
3353
+ listFrag = empty( list );
3354
+ children = listFrag.childNodes;
3355
+ ll = children.length;
3356
+ while ( ll-- ) {
3357
+ child = children[ll];
3358
+ replaceWith( child, empty( child ) );
3203
3359
  }
3360
+ fixContainer( listFrag );
3361
+ replaceWith( list, listFrag );
3204
3362
  }
3363
+ return frag;
3364
+ };
3205
3365
 
3206
- // On keypress, delete and '.' both have event.keyCode 46
3207
- // Must check event.which to differentiate.
3208
- if ( isPresto && event.which === 46 ) {
3209
- key = '.';
3210
- }
3211
-
3212
- // Function keys
3213
- if ( 111 < code && code < 124 ) {
3214
- key = 'f' + ( code - 111 );
3215
- }
3216
-
3217
- // We need to apply the backspace/delete handlers regardless of
3218
- // control key modifiers.
3219
- if ( key !== 'backspace' && key !== 'delete' ) {
3220
- if ( event.altKey ) { modifiers += 'alt-'; }
3221
- if ( event.ctrlKey ) { modifiers += 'ctrl-'; }
3222
- if ( event.metaKey ) { modifiers += 'meta-'; }
3366
+ var increaseListLevel = function ( frag ) {
3367
+ var items = frag.querySelectorAll( 'LI' ),
3368
+ i, l, item,
3369
+ type, newParent,
3370
+ tagAttributes = this._config.tagAttributes,
3371
+ listItemAttrs = tagAttributes.li,
3372
+ listAttrs;
3373
+ for ( i = 0, l = items.length; i < l; i += 1 ) {
3374
+ item = items[i];
3375
+ if ( !isContainer( item.firstChild ) ) {
3376
+ // type => 'UL' or 'OL'
3377
+ type = item.parentNode.nodeName;
3378
+ newParent = item.previousSibling;
3379
+ if ( !newParent || !( newParent = newParent.lastChild ) ||
3380
+ newParent.nodeName !== type ) {
3381
+ listAttrs = tagAttributes[ type.toLowerCase() ];
3382
+ replaceWith(
3383
+ item,
3384
+ this.createElement( 'LI', listItemAttrs, [
3385
+ newParent = this.createElement( type, listAttrs )
3386
+ ])
3387
+ );
3388
+ }
3389
+ newParent.appendChild( item );
3390
+ }
3223
3391
  }
3224
- // However, on Windows, shift-delete is apparently "cut" (WTF right?), so
3225
- // we want to let the browser handle shift-delete.
3226
- if ( event.shiftKey ) { modifiers += 'shift-'; }
3392
+ return frag;
3393
+ };
3227
3394
 
3228
- key = modifiers + key;
3395
+ var decreaseListLevel = function ( frag ) {
3396
+ var items = frag.querySelectorAll( 'LI' );
3397
+ Array.prototype.filter.call( items, function ( el ) {
3398
+ return !isContainer( el.firstChild );
3399
+ }).forEach( function ( item ) {
3400
+ var parent = item.parentNode,
3401
+ newParent = parent.parentNode,
3402
+ first = item.firstChild,
3403
+ node = first,
3404
+ next;
3405
+ if ( item.previousSibling ) {
3406
+ parent = split( parent, item, newParent );
3407
+ }
3408
+ while ( node ) {
3409
+ next = node.nextSibling;
3410
+ if ( isContainer( node ) ) {
3411
+ break;
3412
+ }
3413
+ newParent.insertBefore( node, parent );
3414
+ node = next;
3415
+ }
3416
+ if ( newParent.nodeName === 'LI' && first.previousSibling ) {
3417
+ split( newParent, first, newParent.parentNode );
3418
+ }
3419
+ while ( item !== frag && !item.childNodes.length ) {
3420
+ parent = item.parentNode;
3421
+ parent.removeChild( item );
3422
+ item = parent;
3423
+ }
3424
+ }, this );
3425
+ fixContainer( frag );
3426
+ return frag;
3427
+ };
3229
3428
 
3230
- if ( this._keyHandlers[ key ] ) {
3231
- this._keyHandlers[ key ]( this, event, range );
3232
- } else if ( key.length === 1 && !range.collapsed ) {
3233
- // Record undo checkpoint.
3234
- this._recordUndoState( range );
3235
- this._getRangeAndRemoveBookmark( range );
3236
- // Delete the selection
3237
- deleteContentsOfRange( range );
3238
- this._ensureBottomLine();
3239
- this.setSelection( range );
3240
- this._updatePath( range, true );
3429
+ proto._ensureBottomLine = function () {
3430
+ var body = this._body,
3431
+ last = body.lastElementChild;
3432
+ if ( !last ||
3433
+ last.nodeName !== this._config.blockTag || !isBlock( last ) ) {
3434
+ body.appendChild( this.createDefaultBlock() );
3241
3435
  }
3242
3436
  };
3243
3437
 
3438
+ // --- Keyboard interaction ---
3439
+
3244
3440
  proto.setKeyHandler = function ( key, fn ) {
3245
3441
  this._keyHandlers[ key ] = fn;
3246
3442
  return this;
@@ -3299,7 +3495,7 @@ proto.setHTML = function ( html ) {
3299
3495
  div.innerHTML = html;
3300
3496
  frag.appendChild( empty( div ) );
3301
3497
 
3302
- cleanTree( frag, true );
3498
+ cleanTree( frag );
3303
3499
  cleanupBRs( frag );
3304
3500
 
3305
3501
  fixContainer( frag );
@@ -3394,10 +3590,42 @@ proto.insertImage = function ( src, attributes ) {
3394
3590
  return img;
3395
3591
  };
3396
3592
 
3593
+ var linkRegExp = /\b((?:(?:ht|f)tps?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,}\/)(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\((?:[^\s()<>]+|(?:\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))|([\w\-.%+]+@(?:[\w\-]+\.)+[A-Z]{2,}\b)/i;
3594
+
3595
+ var addLinks = function ( frag ) {
3596
+ var doc = frag.ownerDocument,
3597
+ walker = new TreeWalker( frag, SHOW_TEXT,
3598
+ function ( node ) {
3599
+ return !getNearest( node, 'A' );
3600
+ }, false ),
3601
+ node, data, parent, match, index, endIndex, child;
3602
+ while ( node = walker.nextNode() ) {
3603
+ data = node.data;
3604
+ parent = node.parentNode;
3605
+ while ( match = linkRegExp.exec( data ) ) {
3606
+ index = match.index;
3607
+ endIndex = index + match[0].length;
3608
+ if ( index ) {
3609
+ child = doc.createTextNode( data.slice( 0, index ) );
3610
+ parent.insertBefore( child, node );
3611
+ }
3612
+ child = doc.createElement( 'A' );
3613
+ child.textContent = data.slice( index, endIndex );
3614
+ child.href = match[1] ?
3615
+ /^(?:ht|f)tps?:/.test( match[1] ) ?
3616
+ match[1] :
3617
+ 'http://' + match[1] :
3618
+ 'mailto:' + match[2];
3619
+ parent.insertBefore( child, node );
3620
+ node.data = data = data.slice( endIndex );
3621
+ }
3622
+ }
3623
+ };
3624
+
3397
3625
  // Insert HTML at the cursor location. If the selection is not collapsed
3398
3626
  // insertTreeFragmentIntoRange will delete the selection so that it is replaced
3399
3627
  // by the html being inserted.
3400
- proto.insertHTML = function ( html ) {
3628
+ proto.insertHTML = function ( html, isPaste ) {
3401
3629
  var range = this.getSelection(),
3402
3630
  frag = this._doc.createDocumentFragment(),
3403
3631
  div = this.createElement( 'DIV' );
@@ -3411,24 +3639,37 @@ proto.insertHTML = function ( html ) {
3411
3639
  this._getRangeAndRemoveBookmark( range );
3412
3640
 
3413
3641
  try {
3414
- frag.normalize();
3642
+ var node = frag;
3643
+ var event = {
3644
+ fragment: frag,
3645
+ preventDefault: function () {
3646
+ this.defaultPrevented = true;
3647
+ },
3648
+ defaultPrevented: false
3649
+ };
3650
+
3415
3651
  addLinks( frag );
3416
- cleanTree( frag, true );
3652
+ cleanTree( frag );
3417
3653
  cleanupBRs( frag );
3418
3654
  removeEmptyInlines( frag );
3419
- fixContainer( frag );
3655
+ frag.normalize();
3420
3656
 
3421
- var node = frag;
3422
3657
  while ( node = getNextBlock( node ) ) {
3423
3658
  fixCursor( node );
3424
3659
  }
3425
3660
 
3426
- insertTreeFragmentIntoRange( range, frag );
3427
- if ( !canObserveMutations ) {
3428
- this._docWasChanged();
3661
+ if ( isPaste ) {
3662
+ this.fireEvent( 'willPaste', event );
3663
+ }
3664
+
3665
+ if ( !event.defaultPrevented ) {
3666
+ insertTreeFragmentIntoRange( range, event.fragment );
3667
+ if ( !canObserveMutations ) {
3668
+ this._docWasChanged();
3669
+ }
3670
+ range.collapse( false );
3671
+ this._ensureBottomLine();
3429
3672
  }
3430
- range.collapse( false );
3431
- this._ensureBottomLine();
3432
3673
 
3433
3674
  this.setSelection( range );
3434
3675
  this._updatePath( range, true );
@@ -3438,6 +3679,24 @@ proto.insertHTML = function ( html ) {
3438
3679
  return this;
3439
3680
  };
3440
3681
 
3682
+ proto.insertPlainText = function ( plainText, isPaste ) {
3683
+ var lines = plainText.split( '\n' ),
3684
+ i, l, line;
3685
+ for ( i = 0, l = lines.length; i < l; i += 1 ) {
3686
+ line = lines[i];
3687
+ line = line.split( '&' ).join( '&amp;' )
3688
+ .split( '<' ).join( '&lt;' )
3689
+ .split( '>' ).join( '&gt;' )
3690
+ .replace( / (?= )/g, '&nbsp;' );
3691
+ // Wrap all but first/last lines in <div></div>
3692
+ if ( i && i + 1 < l ) {
3693
+ line = '<DIV>' + ( line || '<BR>' ) + '</DIV>';
3694
+ }
3695
+ lines[i] = line;
3696
+ }
3697
+ return this.insertHTML( lines.join( '' ), isPaste );
3698
+ };
3699
+
3441
3700
  // --- Formatting ---
3442
3701
 
3443
3702
  var command = function ( method, arg, arg2 ) {
@@ -3453,16 +3712,8 @@ proto.addStyles = function ( styles ) {
3453
3712
  style = this.createElement( 'STYLE', {
3454
3713
  type: 'text/css'
3455
3714
  });
3456
- if ( style.styleSheet ) {
3457
- // IE8: must append to document BEFORE adding styles
3458
- // or you get the IE7 CSS parser!
3459
- head.appendChild( style );
3460
- style.styleSheet.cssText = styles;
3461
- } else {
3462
- // Everyone else
3463
- style.appendChild( this._doc.createTextNode( styles ) );
3464
- head.appendChild( style );
3465
- }
3715
+ style.appendChild( this._doc.createTextNode( styles ) );
3716
+ head.appendChild( style );
3466
3717
  }
3467
3718
  return this;
3468
3719
  };
@@ -3547,7 +3798,7 @@ proto.setTextColour = function ( colour ) {
3547
3798
  tag: 'SPAN',
3548
3799
  attributes: {
3549
3800
  'class': 'colour',
3550
- style: 'color: ' + colour
3801
+ style: 'color:' + colour
3551
3802
  }
3552
3803
  }, {
3553
3804
  tag: 'SPAN',
@@ -3561,7 +3812,7 @@ proto.setHighlightColour = function ( colour ) {
3561
3812
  tag: 'SPAN',
3562
3813
  attributes: {
3563
3814
  'class': 'highlight',
3564
- style: 'background-color: ' + colour
3815
+ style: 'background-color:' + colour
3565
3816
  }
3566
3817
  }, {
3567
3818
  tag: 'SPAN',
@@ -3591,6 +3842,112 @@ proto.setTextDirection = function ( direction ) {
3591
3842
  return this.focus();
3592
3843
  };
3593
3844
 
3845
+ function removeFormatting ( self, root, clean ) {
3846
+ var node, next;
3847
+ for ( node = root.firstChild; node; node = next ) {
3848
+ next = node.nextSibling;
3849
+ if ( isInline( node ) ) {
3850
+ if ( node.nodeType === TEXT_NODE || node.nodeName === 'BR' || node.nodeName === 'IMG' ) {
3851
+ clean.appendChild( node );
3852
+ continue;
3853
+ }
3854
+ } else if ( isBlock( node ) ) {
3855
+ clean.appendChild( self.createDefaultBlock([
3856
+ removeFormatting(
3857
+ self, node, self._doc.createDocumentFragment() )
3858
+ ]));
3859
+ continue;
3860
+ }
3861
+ removeFormatting( self, node, clean );
3862
+ }
3863
+ return clean;
3864
+ }
3865
+
3866
+ proto.removeAllFormatting = function ( range ) {
3867
+ if ( !range && !( range = this.getSelection() ) || range.collapsed ) {
3868
+ return this;
3869
+ }
3870
+
3871
+ var stopNode = range.commonAncestorContainer;
3872
+ while ( stopNode && !isBlock( stopNode ) ) {
3873
+ stopNode = stopNode.parentNode;
3874
+ }
3875
+ if ( !stopNode ) {
3876
+ expandRangeToBlockBoundaries( range );
3877
+ stopNode = this._body;
3878
+ }
3879
+ if ( stopNode.nodeType === TEXT_NODE ) {
3880
+ return this;
3881
+ }
3882
+
3883
+ // Record undo point
3884
+ this._recordUndoState( range );
3885
+ this._getRangeAndRemoveBookmark( range );
3886
+
3887
+
3888
+ // Avoid splitting where we're already at edges.
3889
+ moveRangeBoundariesUpTree( range, stopNode );
3890
+
3891
+ // Split the selection up to the block, or if whole selection in same
3892
+ // block, expand range boundaries to ends of block and split up to body.
3893
+ var doc = stopNode.ownerDocument;
3894
+ var startContainer = range.startContainer;
3895
+ var startOffset = range.startOffset;
3896
+ var endContainer = range.endContainer;
3897
+ var endOffset = range.endOffset;
3898
+
3899
+ // Split end point first to avoid problems when end and start
3900
+ // in same container.
3901
+ var formattedNodes = doc.createDocumentFragment();
3902
+ var cleanNodes = doc.createDocumentFragment();
3903
+ var nodeAfterSplit = split( endContainer, endOffset, stopNode );
3904
+ var nodeInSplit = split( startContainer, startOffset, stopNode );
3905
+ var nextNode, _range, childNodes;
3906
+
3907
+ // Then replace contents in split with a cleaned version of the same:
3908
+ // blocks become default blocks, text and leaf nodes survive, everything
3909
+ // else is obliterated.
3910
+ while ( nodeInSplit !== nodeAfterSplit ) {
3911
+ nextNode = nodeInSplit.nextSibling;
3912
+ formattedNodes.appendChild( nodeInSplit );
3913
+ nodeInSplit = nextNode;
3914
+ }
3915
+ removeFormatting( this, formattedNodes, cleanNodes );
3916
+ cleanNodes.normalize();
3917
+ nodeInSplit = cleanNodes.firstChild;
3918
+ nextNode = cleanNodes.lastChild;
3919
+
3920
+ // Restore selection
3921
+ childNodes = stopNode.childNodes;
3922
+ if ( nodeInSplit ) {
3923
+ stopNode.insertBefore( cleanNodes, nodeAfterSplit );
3924
+ startOffset = indexOf.call( childNodes, nodeInSplit );
3925
+ endOffset = indexOf.call( childNodes, nextNode ) + 1;
3926
+ } else {
3927
+ startOffset = indexOf.call( childNodes, nodeAfterSplit );
3928
+ endOffset = startOffset;
3929
+ }
3930
+
3931
+ // Merge text nodes at edges, if possible
3932
+ _range = {
3933
+ startContainer: stopNode,
3934
+ startOffset: startOffset,
3935
+ endContainer: stopNode,
3936
+ endOffset: endOffset
3937
+ };
3938
+ mergeInlines( stopNode, _range );
3939
+ range.setStart( _range.startContainer, _range.startOffset );
3940
+ range.setEnd( _range.endContainer, _range.endOffset );
3941
+
3942
+ // And move back down the tree
3943
+ moveRangeBoundariesDownTree( range );
3944
+
3945
+ this.setSelection( range );
3946
+ this._updatePath( range, true );
3947
+
3948
+ return this.focus();
3949
+ };
3950
+
3594
3951
  proto.increaseQuoteLevel = command( 'modifyBlocks', increaseBlockQuoteLevel );
3595
3952
  proto.decreaseQuoteLevel = command( 'modifyBlocks', decreaseBlockQuoteLevel );
3596
3953
 
@@ -3601,18 +3958,23 @@ proto.removeList = command( 'modifyBlocks', removeList );
3601
3958
  proto.increaseListLevel = command( 'modifyBlocks', increaseListLevel );
3602
3959
  proto.decreaseListLevel = command( 'modifyBlocks', decreaseListLevel );
3603
3960
 
3604
- if ( top !== win ) {
3605
- win.editor = new Squire( doc );
3606
- if ( win.onEditorLoad ) {
3607
- win.onEditorLoad( win.editor );
3608
- win.onEditorLoad = null;
3609
- }
3961
+ if ( typeof exports === 'object' ) {
3962
+ module.exports = Squire;
3963
+ } else if ( typeof define === 'function' && define.amd ) {
3964
+ define( function () {
3965
+ return Squire;
3966
+ });
3610
3967
  } else {
3611
- if ( typeof exports === 'object' ) {
3612
- module.exports = Squire;
3613
- } else {
3614
- win.Squire = Squire;
3968
+ win.Squire = Squire;
3969
+
3970
+ if ( top !== win &&
3971
+ doc.documentElement.getAttribute( 'data-squireinit' ) === 'true' ) {
3972
+ win.editor = new Squire( doc );
3973
+ if ( win.onEditorLoad ) {
3974
+ win.onEditorLoad( win.editor );
3975
+ win.onEditorLoad = null;
3976
+ }
3615
3977
  }
3616
3978
  }
3617
3979
 
3618
- }( document ) );
3980
+ }( document ) );