squire-rails 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 ) );