squire-rails 0.0.6 → 0.0.7

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.
@@ -1,4 +1,4 @@
1
- /* Copyright © 2011-2013 by Neil Jenkins. MIT Licensed. */
1
+ /* Copyright © 2011-2015 by Neil Jenkins. MIT Licensed. */
2
2
 
3
3
  ( function ( doc, undefined ) {
4
4
 
@@ -42,6 +42,15 @@ var notWS = /[^ \t\r\n]/;
42
42
 
43
43
  var indexOf = Array.prototype.indexOf;
44
44
 
45
+ // Polyfill for FF3.5
46
+ if ( !Object.create ) {
47
+ Object.create = function ( proto ) {
48
+ var F = function () {};
49
+ F.prototype = proto;
50
+ return new F();
51
+ };
52
+ }
53
+
45
54
  /*
46
55
  Native TreeWalker is buggy in IE and Opera:
47
56
  * IE9/10 sometimes throw errors when calling TreeWalker#nextNode or
@@ -213,7 +222,7 @@ function getNearest ( node, tag, attributes ) {
213
222
 
214
223
  function getPath ( node ) {
215
224
  var parent = node.parentNode,
216
- path, id, className, classNames;
225
+ path, id, className, classNames, dir;
217
226
  if ( !parent || node.nodeType !== ELEMENT_NODE ) {
218
227
  path = parent ? getPath( parent ) : '';
219
228
  } else {
@@ -228,6 +237,9 @@ function getPath ( node ) {
228
237
  path += '.';
229
238
  path += classNames.join( '.' );
230
239
  }
240
+ if ( dir = node.dir ) {
241
+ path += '[dir=' + dir + ']';
242
+ }
231
243
  }
232
244
  return path;
233
245
  }
@@ -291,14 +303,11 @@ function fixCursor ( node ) {
291
303
  // cursor to appear.
292
304
  var doc = node.ownerDocument,
293
305
  root = node,
294
- fixer, child, instance;
306
+ fixer, child;
295
307
 
296
308
  if ( node.nodeName === 'BODY' ) {
297
309
  if ( !( child = node.firstChild ) || child.nodeName === 'BR' ) {
298
- instance = getSquireInstance( doc );
299
- fixer = instance ?
300
- instance.createDefaultBlock() :
301
- createElement( doc, 'DIV' );
310
+ fixer = getSquireInstance( doc ).createDefaultBlock();
302
311
  if ( child ) {
303
312
  node.replaceChild( fixer, child );
304
313
  }
@@ -364,17 +373,25 @@ function fixContainer ( container ) {
364
373
  var children = container.childNodes,
365
374
  doc = container.ownerDocument,
366
375
  wrapper = null,
367
- i, l, child, isBR;
376
+ i, l, child, isBR,
377
+ config = getSquireInstance( doc )._config;
378
+
368
379
  for ( i = 0, l = children.length; i < l; i += 1 ) {
369
380
  child = children[i];
370
381
  isBR = child.nodeName === 'BR';
371
382
  if ( !isBR && isInline( child ) ) {
372
- if ( !wrapper ) { wrapper = createElement( doc, 'DIV' ); }
383
+ if ( !wrapper ) {
384
+ wrapper = createElement( doc,
385
+ config.blockTag, config.blockAttributes );
386
+ }
373
387
  wrapper.appendChild( child );
374
388
  i -= 1;
375
389
  l -= 1;
376
390
  } else if ( isBR || wrapper ) {
377
- if ( !wrapper ) { wrapper = createElement( doc, 'DIV' ); }
391
+ if ( !wrapper ) {
392
+ wrapper = createElement( doc,
393
+ config.blockTag, config.blockAttributes );
394
+ }
378
395
  fixCursor( wrapper );
379
396
  if ( isBR ) {
380
397
  container.replaceChild( wrapper, child );
@@ -1056,2066 +1073,2120 @@ var expandRangeToBlockBoundaries = function ( range ) {
1056
1073
  }
1057
1074
  };
1058
1075
 
1059
- var instances = [];
1076
+ var mapKeyTo = function ( method ) {
1077
+ return function ( self, event ) {
1078
+ event.preventDefault();
1079
+ self[ method ]();
1080
+ };
1081
+ };
1060
1082
 
1061
- function getSquireInstance ( doc ) {
1062
- var l = instances.length,
1063
- instance;
1064
- while ( l-- ) {
1065
- instance = instances[l];
1066
- if ( instance._doc === doc ) {
1067
- return instance;
1083
+ var mapKeyToFormat = function ( tag, remove ) {
1084
+ remove = remove || null;
1085
+ return function ( self, event ) {
1086
+ event.preventDefault();
1087
+ var range = self.getSelection();
1088
+ if ( self.hasFormat( tag, null, range ) ) {
1089
+ self.changeFormat( null, { tag: tag }, range );
1090
+ } else {
1091
+ self.changeFormat( { tag: tag }, remove, range );
1068
1092
  }
1069
- }
1070
- return null;
1071
- }
1072
-
1073
- function Squire ( doc ) {
1074
- var win = doc.defaultView;
1075
- var body = doc.body;
1076
- var mutation;
1077
-
1078
- this._win = win;
1079
- this._doc = doc;
1080
- this._body = body;
1081
-
1082
- this._events = {};
1083
-
1084
- this._sel = win.getSelection();
1085
- this._lastSelection = null;
1093
+ };
1094
+ };
1086
1095
 
1087
- // IE loses selection state of iframe on blur, so make sure we
1088
- // cache it just before it loses focus.
1089
- if ( losesSelectionOnBlur ) {
1090
- this.addEventListener( 'beforedeactivate', this.getSelection );
1096
+ // If you delete the content inside a span with a font styling, Webkit will
1097
+ // replace it with a <font> tag (!). If you delete all the text inside a
1098
+ // link in Opera, it won't delete the link. Let's make things consistent. If
1099
+ // you delete all text inside an inline tag, remove the inline tag.
1100
+ var afterDelete = function ( self, range ) {
1101
+ try {
1102
+ if ( !range ) { range = self.getSelection(); }
1103
+ var node = range.startContainer,
1104
+ parent;
1105
+ // Climb the tree from the focus point while we are inside an empty
1106
+ // inline element
1107
+ if ( node.nodeType === TEXT_NODE ) {
1108
+ node = node.parentNode;
1109
+ }
1110
+ parent = node;
1111
+ while ( isInline( parent ) &&
1112
+ ( !parent.textContent || parent.textContent === ZWS ) ) {
1113
+ node = parent;
1114
+ parent = node.parentNode;
1115
+ }
1116
+ // If focussed in empty inline element
1117
+ if ( node !== parent ) {
1118
+ // Move focus to just before empty inline(s)
1119
+ range.setStart( parent,
1120
+ indexOf.call( parent.childNodes, node ) );
1121
+ range.collapse( true );
1122
+ // Remove empty inline(s)
1123
+ parent.removeChild( node );
1124
+ // Fix cursor in block
1125
+ if ( !isBlock( parent ) ) {
1126
+ parent = getPreviousBlock( parent );
1127
+ }
1128
+ fixCursor( parent );
1129
+ // Move cursor into text node
1130
+ moveRangeBoundariesDownTree( range );
1131
+ }
1132
+ self._ensureBottomLine();
1133
+ self.setSelection( range );
1134
+ self._updatePath( range, true );
1135
+ } catch ( error ) {
1136
+ self.didError( error );
1091
1137
  }
1138
+ };
1092
1139
 
1093
- this._hasZWS = false;
1094
-
1095
- this._lastAnchorNode = null;
1096
- this._lastFocusNode = null;
1097
- this._path = '';
1098
-
1099
- this.addEventListener( 'keyup', this._updatePathOnEvent );
1100
- this.addEventListener( 'mouseup', this._updatePathOnEvent );
1140
+ var keyHandlers = {
1141
+ enter: function ( self, event, range ) {
1142
+ var block, parent, nodeAfterSplit;
1101
1143
 
1102
- win.addEventListener( 'focus', this, false );
1103
- win.addEventListener( 'blur', this, false );
1144
+ // We handle this ourselves
1145
+ event.preventDefault();
1104
1146
 
1105
- this._undoIndex = -1;
1106
- this._undoStack = [];
1107
- this._undoStackLength = 0;
1108
- this._isInUndoState = false;
1109
- this._ignoreChange = false;
1147
+ // Save undo checkpoint and add any links in the preceding section.
1148
+ // Remove any zws so we don't think there's content in an empty
1149
+ // block.
1150
+ self._recordUndoState( range );
1151
+ addLinks( range.startContainer );
1152
+ self._removeZWS();
1153
+ self._getRangeAndRemoveBookmark( range );
1110
1154
 
1111
- if ( canObserveMutations ) {
1112
- mutation = new MutationObserver( this._docWasChanged.bind( this ) );
1113
- mutation.observe( body, {
1114
- childList: true,
1115
- attributes: true,
1116
- characterData: true,
1117
- subtree: true
1118
- });
1119
- this._mutation = mutation;
1120
- } else {
1121
- this.addEventListener( 'keyup', this._keyUpDetectChange );
1122
- }
1155
+ // Selected text is overwritten, therefore delete the contents
1156
+ // to collapse selection.
1157
+ if ( !range.collapsed ) {
1158
+ deleteContentsOfRange( range );
1159
+ }
1123
1160
 
1124
- this.defaultBlockTag = 'DIV';
1125
- this.defaultBlockProperties = null;
1161
+ block = getStartBlockOfRange( range );
1126
1162
 
1127
- // IE sometimes fires the beforepaste event twice; make sure it is not run
1128
- // again before our after paste function is called.
1129
- this._awaitingPaste = false;
1130
- this.addEventListener( isIElt11 ? 'beforecut' : 'cut', this._onCut );
1131
- this.addEventListener( isIElt11 ? 'beforepaste' : 'paste', this._onPaste );
1163
+ // If this is a malformed bit of document or in a table;
1164
+ // just play it safe and insert a <br>.
1165
+ if ( !block || /^T[HD]$/.test( block.nodeName ) ) {
1166
+ insertNodeInRange( range, self.createElement( 'BR' ) );
1167
+ range.collapse( false );
1168
+ self.setSelection( range );
1169
+ self._updatePath( range, true );
1170
+ return;
1171
+ }
1132
1172
 
1133
- // Opera does not fire keydown repeatedly.
1134
- this.addEventListener( isPresto ? 'keypress' : 'keydown', this._onKey );
1173
+ // If in a list, we'll split the LI instead.
1174
+ if ( parent = getNearest( block, 'LI' ) ) {
1175
+ block = parent;
1176
+ }
1135
1177
 
1136
- // Fix IE<10's buggy implementation of Text#splitText.
1137
- // If the split is at the end of the node, it doesn't insert the newly split
1138
- // node into the document, and sets its value to undefined rather than ''.
1139
- // And even if the split is not at the end, the original node is removed
1140
- // from the document and replaced by another, rather than just having its
1141
- // data shortened.
1142
- // We used to feature test for this, but then found the feature test would
1143
- // sometimes pass, but later on the buggy behaviour would still appear.
1144
- // I think IE10 does not have the same bug, but it doesn't hurt to replace
1145
- // its native fn too and then we don't need yet another UA category.
1146
- if ( isIElt11 ) {
1147
- win.Text.prototype.splitText = function ( offset ) {
1148
- var afterSplit = this.ownerDocument.createTextNode(
1149
- this.data.slice( offset ) ),
1150
- next = this.nextSibling,
1151
- parent = this.parentNode,
1152
- toDelete = this.length - offset;
1153
- if ( next ) {
1154
- parent.insertBefore( afterSplit, next );
1155
- } else {
1156
- parent.appendChild( afterSplit );
1178
+ if ( !block.textContent ) {
1179
+ // Break list
1180
+ if ( getNearest( block, 'UL' ) || getNearest( block, 'OL' ) ) {
1181
+ return self.modifyBlocks( decreaseListLevel, range );
1157
1182
  }
1158
- if ( toDelete ) {
1159
- this.deleteData( offset, toDelete );
1183
+ // Break blockquote
1184
+ else if ( getNearest( block, 'BLOCKQUOTE' ) ) {
1185
+ return self.modifyBlocks( removeBlockQuote, range );
1160
1186
  }
1161
- return afterSplit;
1162
- };
1163
- }
1187
+ }
1164
1188
 
1165
- body.setAttribute( 'contenteditable', 'true' );
1166
- this.setHTML( '' );
1189
+ // Otherwise, split at cursor point.
1190
+ nodeAfterSplit = splitBlock( self, block,
1191
+ range.startContainer, range.startOffset );
1167
1192
 
1168
- // Remove Firefox's built-in controls
1169
- try {
1170
- doc.execCommand( 'enableObjectResizing', false, 'false' );
1171
- doc.execCommand( 'enableInlineTableEditing', false, 'false' );
1172
- } catch ( error ) {}
1193
+ // Clean up any empty inlines if we hit enter at the beginning of the
1194
+ // block
1195
+ removeZWS( block );
1196
+ removeEmptyInlines( block );
1197
+ fixCursor( block );
1173
1198
 
1174
- instances.push( this );
1175
- }
1199
+ // Focus cursor
1200
+ // If there's a <b>/<i> etc. at the beginning of the split
1201
+ // make sure we focus inside it.
1202
+ while ( nodeAfterSplit.nodeType === ELEMENT_NODE ) {
1203
+ var child = nodeAfterSplit.firstChild,
1204
+ next;
1176
1205
 
1177
- var proto = Squire.prototype;
1206
+ // Don't continue links over a block break; unlikely to be the
1207
+ // desired outcome.
1208
+ if ( nodeAfterSplit.nodeName === 'A' &&
1209
+ ( !nodeAfterSplit.textContent ||
1210
+ nodeAfterSplit.textContent === ZWS ) ) {
1211
+ child = self._doc.createTextNode( '' );
1212
+ replaceWith( nodeAfterSplit, child );
1213
+ nodeAfterSplit = child;
1214
+ break;
1215
+ }
1178
1216
 
1179
- proto.createElement = function ( tag, props, children ) {
1180
- return createElement( this._doc, tag, props, children );
1181
- };
1217
+ while ( child && child.nodeType === TEXT_NODE && !child.data ) {
1218
+ next = child.nextSibling;
1219
+ if ( !next || next.nodeName === 'BR' ) {
1220
+ break;
1221
+ }
1222
+ detach( child );
1223
+ child = next;
1224
+ }
1182
1225
 
1183
- proto.createDefaultBlock = function ( children ) {
1184
- return fixCursor(
1185
- this.createElement(
1186
- this.defaultBlockTag, this.defaultBlockProperties, children )
1187
- );
1188
- };
1226
+ // 'BR's essentially don't count; they're a browser hack.
1227
+ // If you try to select the contents of a 'BR', FF will not let
1228
+ // you type anything!
1229
+ if ( !child || child.nodeName === 'BR' ||
1230
+ ( child.nodeType === TEXT_NODE && !isPresto ) ) {
1231
+ break;
1232
+ }
1233
+ nodeAfterSplit = child;
1234
+ }
1235
+ range = self._createRange( nodeAfterSplit, 0 );
1236
+ self.setSelection( range );
1237
+ self._updatePath( range, true );
1189
1238
 
1190
- proto.didError = function ( error ) {
1191
- console.log( error );
1192
- };
1193
-
1194
- proto.getDocument = function () {
1195
- return this._doc;
1196
- };
1197
-
1198
- // --- Events ---
1199
-
1200
- // Subscribing to these events won't automatically add a listener to the
1201
- // document node, since these events are fired in a custom manner by the
1202
- // editor code.
1203
- var customEvents = {
1204
- focus: 1, blur: 1,
1205
- pathChange: 1, select: 1, input: 1, undoStateChange: 1
1206
- };
1207
-
1208
- proto.fireEvent = function ( type, event ) {
1209
- var handlers = this._events[ type ],
1210
- i, l, obj;
1211
- if ( handlers ) {
1212
- if ( !event ) {
1213
- event = {};
1239
+ // Scroll into view
1240
+ if ( nodeAfterSplit.nodeType === TEXT_NODE ) {
1241
+ nodeAfterSplit = nodeAfterSplit.parentNode;
1214
1242
  }
1215
- if ( event.type !== type ) {
1216
- event.type = type;
1243
+ var doc = self._doc,
1244
+ body = self._body;
1245
+ if ( nodeAfterSplit.offsetTop + nodeAfterSplit.offsetHeight >
1246
+ ( doc.documentElement.scrollTop || body.scrollTop ) +
1247
+ body.offsetHeight ) {
1248
+ nodeAfterSplit.scrollIntoView( false );
1217
1249
  }
1218
- // Clone handlers array, so any handlers added/removed do not affect it.
1219
- handlers = handlers.slice();
1220
- for ( i = 0, l = handlers.length; i < l; i += 1 ) {
1221
- obj = handlers[i];
1222
- try {
1223
- if ( obj.handleEvent ) {
1224
- obj.handleEvent( event );
1225
- } else {
1226
- obj.call( this, event );
1250
+ },
1251
+ backspace: function ( self, event, range ) {
1252
+ self._removeZWS();
1253
+ // Record undo checkpoint.
1254
+ self._recordUndoState( range );
1255
+ self._getRangeAndRemoveBookmark( range );
1256
+ // If not collapsed, delete contents
1257
+ if ( !range.collapsed ) {
1258
+ event.preventDefault();
1259
+ deleteContentsOfRange( range );
1260
+ afterDelete( self, range );
1261
+ }
1262
+ // If at beginning of block, merge with previous
1263
+ else if ( rangeDoesStartAtBlockBoundary( range ) ) {
1264
+ event.preventDefault();
1265
+ var current = getStartBlockOfRange( range ),
1266
+ previous = current && getPreviousBlock( current );
1267
+ // Must not be at the very beginning of the text area.
1268
+ if ( previous ) {
1269
+ // If not editable, just delete whole block.
1270
+ if ( !previous.isContentEditable ) {
1271
+ detach( previous );
1272
+ return;
1227
1273
  }
1228
- } catch ( error ) {
1229
- error.details = 'Squire: fireEvent error. Event type: ' + type;
1230
- this.didError( error );
1274
+ // Otherwise merge.
1275
+ mergeWithBlock( previous, current, range );
1276
+ // If deleted line between containers, merge newly adjacent
1277
+ // containers.
1278
+ current = previous.parentNode;
1279
+ while ( current && !current.nextSibling ) {
1280
+ current = current.parentNode;
1281
+ }
1282
+ if ( current && ( current = current.nextSibling ) ) {
1283
+ mergeContainers( current );
1284
+ }
1285
+ self.setSelection( range );
1286
+ }
1287
+ // If at very beginning of text area, allow backspace
1288
+ // to break lists/blockquote.
1289
+ else if ( current ) {
1290
+ // Break list
1291
+ if ( getNearest( current, 'UL' ) ||
1292
+ getNearest( current, 'OL' ) ) {
1293
+ return self.modifyBlocks( decreaseListLevel, range );
1294
+ }
1295
+ // Break blockquote
1296
+ else if ( getNearest( current, 'BLOCKQUOTE' ) ) {
1297
+ return self.modifyBlocks( decreaseBlockQuoteLevel, range );
1298
+ }
1299
+ self.setSelection( range );
1300
+ self._updatePath( range, true );
1231
1301
  }
1232
1302
  }
1233
- }
1234
- return this;
1235
- };
1236
-
1237
- proto.destroy = function () {
1238
- var win = this._win,
1239
- doc = this._doc,
1240
- events = this._events,
1241
- type;
1242
- win.removeEventListener( 'focus', this, false );
1243
- win.removeEventListener( 'blur', this, false );
1244
- for ( type in events ) {
1245
- if ( !customEvents[ type ] ) {
1246
- doc.removeEventListener( type, this, true );
1247
- }
1248
- }
1249
- if ( this._mutation ) {
1250
- this._mutation.disconnect();
1251
- }
1252
- var l = instances.length;
1253
- while ( l-- ) {
1254
- if ( instances[l] === this ) {
1255
- instances.splice( l, 1 );
1303
+ // Otherwise, leave to browser but check afterwards whether it has
1304
+ // left behind an empty inline tag.
1305
+ else {
1306
+ self.setSelection( range );
1307
+ setTimeout( function () { afterDelete( self ); }, 0 );
1256
1308
  }
1257
- }
1258
- };
1259
-
1260
- proto.handleEvent = function ( event ) {
1261
- this.fireEvent( event.type, event );
1262
- };
1263
-
1264
- proto.addEventListener = function ( type, fn ) {
1265
- var handlers = this._events[ type ];
1266
- if ( !fn ) {
1267
- this.didError({
1268
- name: 'Squire: addEventListener with null or undefined fn',
1269
- message: 'Event type: ' + type
1270
- });
1271
- return this;
1272
- }
1273
- if ( !handlers ) {
1274
- handlers = this._events[ type ] = [];
1275
- if ( !customEvents[ type ] ) {
1276
- this._doc.addEventListener( type, this, true );
1309
+ },
1310
+ 'delete': function ( self, event, range ) {
1311
+ self._removeZWS();
1312
+ // Record undo checkpoint.
1313
+ self._recordUndoState( range );
1314
+ self._getRangeAndRemoveBookmark( range );
1315
+ // If not collapsed, delete contents
1316
+ if ( !range.collapsed ) {
1317
+ event.preventDefault();
1318
+ deleteContentsOfRange( range );
1319
+ afterDelete( self, range );
1277
1320
  }
1278
- }
1279
- handlers.push( fn );
1280
- return this;
1281
- };
1282
-
1283
- proto.removeEventListener = function ( type, fn ) {
1284
- var handlers = this._events[ type ],
1285
- l;
1286
- if ( handlers ) {
1287
- l = handlers.length;
1288
- while ( l-- ) {
1289
- if ( handlers[l] === fn ) {
1290
- handlers.splice( l, 1 );
1321
+ // If at end of block, merge next into this block
1322
+ else if ( rangeDoesEndAtBlockBoundary( range ) ) {
1323
+ event.preventDefault();
1324
+ var current = getStartBlockOfRange( range ),
1325
+ next = current && getNextBlock( current );
1326
+ // Must not be at the very end of the text area.
1327
+ if ( next ) {
1328
+ // If not editable, just delete whole block.
1329
+ if ( !next.isContentEditable ) {
1330
+ detach( next );
1331
+ return;
1332
+ }
1333
+ // Otherwise merge.
1334
+ mergeWithBlock( current, next, range );
1335
+ // If deleted line between containers, merge newly adjacent
1336
+ // containers.
1337
+ next = current.parentNode;
1338
+ while ( next && !next.nextSibling ) {
1339
+ next = next.parentNode;
1340
+ }
1341
+ if ( next && ( next = next.nextSibling ) ) {
1342
+ mergeContainers( next );
1343
+ }
1344
+ self.setSelection( range );
1345
+ self._updatePath( range, true );
1291
1346
  }
1292
1347
  }
1293
- if ( !handlers.length ) {
1294
- delete this._events[ type ];
1295
- if ( !customEvents[ type ] ) {
1296
- this._doc.removeEventListener( type, this, false );
1348
+ // Otherwise, leave to browser but check afterwards whether it has
1349
+ // left behind an empty inline tag.
1350
+ else {
1351
+ self.setSelection( range );
1352
+ setTimeout( function () { afterDelete( self ); }, 0 );
1353
+ }
1354
+ },
1355
+ tab: function ( self, event, range ) {
1356
+ var node, parent;
1357
+ self._removeZWS();
1358
+ // If no selection and in an empty block
1359
+ if ( range.collapsed &&
1360
+ rangeDoesStartAtBlockBoundary( range ) &&
1361
+ rangeDoesEndAtBlockBoundary( range ) ) {
1362
+ node = getStartBlockOfRange( range );
1363
+ // Iterate through the block's parents
1364
+ while ( parent = node.parentNode ) {
1365
+ // If we find a UL or OL (so are in a list, node must be an LI)
1366
+ if ( parent.nodeName === 'UL' || parent.nodeName === 'OL' ) {
1367
+ // AND the LI is not the first in the list
1368
+ if ( node.previousSibling ) {
1369
+ // Then increase the list level
1370
+ event.preventDefault();
1371
+ self.modifyBlocks( increaseListLevel, range );
1372
+ }
1373
+ break;
1374
+ }
1375
+ node = parent;
1297
1376
  }
1377
+ event.preventDefault();
1298
1378
  }
1299
- }
1300
- return this;
1301
- };
1302
-
1303
- // --- Selection and Path ---
1304
-
1305
- proto._createRange =
1306
- function ( range, startOffset, endContainer, endOffset ) {
1307
- if ( range instanceof this._win.Range ) {
1308
- return range.cloneRange();
1309
- }
1310
- var domRange = this._doc.createRange();
1311
- domRange.setStart( range, startOffset );
1312
- if ( endContainer ) {
1313
- domRange.setEnd( endContainer, endOffset );
1314
- } else {
1315
- domRange.setEnd( range, startOffset );
1316
- }
1317
- return domRange;
1318
- };
1379
+ },
1380
+ space: function ( self, _, range ) {
1381
+ var node, parent;
1382
+ self._recordUndoState( range );
1383
+ addLinks( range.startContainer );
1384
+ self._getRangeAndRemoveBookmark( range );
1319
1385
 
1320
- proto.setSelection = function ( range ) {
1321
- if ( range ) {
1322
- // iOS bug: if you don't focus the iframe before setting the
1323
- // selection, you can end up in a state where you type but the input
1324
- // doesn't get directed into the contenteditable area but is instead
1325
- // lost in a black hole. Very strange.
1326
- if ( isIOS ) {
1327
- this._win.focus();
1386
+ // If the cursor is at the end of a link (<a>foo|</a>) then move it
1387
+ // outside of the link (<a>foo</a>|) so that the space is not part of
1388
+ // the link text.
1389
+ node = range.endContainer;
1390
+ parent = node.parentNode;
1391
+ if ( range.collapsed && parent.nodeName === 'A' &&
1392
+ !node.nextSibling && range.endOffset === getLength( node ) ) {
1393
+ range.setStartAfter( parent );
1328
1394
  }
1329
- var sel = this._sel;
1330
- sel.removeAllRanges();
1331
- sel.addRange( range );
1332
- }
1333
- return this;
1334
- };
1335
1395
 
1336
- proto.getSelection = function () {
1337
- var sel = this._sel,
1338
- selection, startContainer, endContainer;
1339
- if ( sel.rangeCount ) {
1340
- selection = sel.getRangeAt( 0 ).cloneRange();
1341
- startContainer = selection.startContainer;
1342
- endContainer = selection.endContainer;
1343
- // FF can return the selection as being inside an <img>. WTF?
1344
- if ( startContainer && isLeaf( startContainer ) ) {
1345
- selection.setStartBefore( startContainer );
1346
- }
1347
- if ( endContainer && isLeaf( endContainer ) ) {
1348
- selection.setEndBefore( endContainer );
1349
- }
1350
- this._lastSelection = selection;
1351
- } else {
1352
- selection = this._lastSelection;
1353
- }
1354
- if ( !selection ) {
1355
- selection = this._createRange( this._body.firstChild, 0 );
1396
+ self.setSelection( range );
1397
+ },
1398
+ left: function ( self ) {
1399
+ self._removeZWS();
1400
+ },
1401
+ right: function ( self ) {
1402
+ self._removeZWS();
1356
1403
  }
1357
- return selection;
1358
1404
  };
1359
1405
 
1360
- proto.getSelectedText = function () {
1361
- var range = this.getSelection(),
1362
- walker = new TreeWalker(
1363
- range.commonAncestorContainer,
1364
- SHOW_TEXT|SHOW_ELEMENT,
1365
- function ( node ) {
1366
- return isNodeContainedInRange( range, node, true );
1367
- }
1368
- ),
1369
- startContainer = range.startContainer,
1370
- endContainer = range.endContainer,
1371
- node = walker.currentNode = startContainer,
1372
- textContent = '',
1373
- addedTextInBlock = false,
1374
- value;
1406
+ // Firefox incorrectly handles Cmd-left/Cmd-right on Mac:
1407
+ // it goes back/forward in history! Override to do the right
1408
+ // thing.
1409
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=289384
1410
+ if ( isMac && isGecko && win.getSelection().modify ) {
1411
+ keyHandlers[ 'meta-left' ] = function ( self, event ) {
1412
+ event.preventDefault();
1413
+ self._sel.modify( 'move', 'backward', 'lineboundary' );
1414
+ };
1415
+ keyHandlers[ 'meta-right' ] = function ( self, event ) {
1416
+ event.preventDefault();
1417
+ self._sel.modify( 'move', 'forward', 'lineboundary' );
1418
+ };
1419
+ }
1375
1420
 
1376
- if ( !walker.filter( node ) ) {
1377
- node = walker.nextNode();
1378
- }
1421
+ keyHandlers[ ctrlKey + 'b' ] = mapKeyToFormat( 'B' );
1422
+ keyHandlers[ ctrlKey + 'i' ] = mapKeyToFormat( 'I' );
1423
+ keyHandlers[ ctrlKey + 'u' ] = mapKeyToFormat( 'U' );
1424
+ keyHandlers[ ctrlKey + 'shift-7' ] = mapKeyToFormat( 'S' );
1425
+ keyHandlers[ ctrlKey + 'shift-5' ] = mapKeyToFormat( 'SUB', { tag: 'SUP' } );
1426
+ keyHandlers[ ctrlKey + 'shift-6' ] = mapKeyToFormat( 'SUP', { tag: 'SUB' } );
1427
+ keyHandlers[ ctrlKey + 'shift-8' ] = mapKeyTo( 'makeUnorderedList' );
1428
+ keyHandlers[ ctrlKey + 'shift-9' ] = mapKeyTo( 'makeOrderedList' );
1429
+ keyHandlers[ ctrlKey + '[' ] = mapKeyTo( 'decreaseQuoteLevel' );
1430
+ keyHandlers[ ctrlKey + ']' ] = mapKeyTo( 'increaseQuoteLevel' );
1431
+ keyHandlers[ ctrlKey + 'y' ] = mapKeyTo( 'redo' );
1432
+ keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( 'undo' );
1433
+ keyHandlers[ ctrlKey + 'shift-z' ] = mapKeyTo( 'redo' );
1379
1434
 
1380
- while ( node ) {
1381
- if ( node.nodeType === TEXT_NODE ) {
1382
- value = node.data;
1383
- if ( value && ( /\S/.test( value ) ) ) {
1384
- if ( node === endContainer ) {
1385
- value = value.slice( 0, range.endOffset );
1386
- }
1387
- if ( node === startContainer ) {
1388
- value = value.slice( range.startOffset );
1389
- }
1390
- textContent += value;
1391
- addedTextInBlock = true;
1392
- }
1393
- } else if ( node.nodeName === 'BR' ||
1394
- addedTextInBlock && !isInline( node ) ) {
1395
- textContent += '\n';
1396
- addedTextInBlock = false;
1435
+ var instances = [];
1436
+
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;
1397
1444
  }
1398
- node = walker.nextNode();
1399
1445
  }
1446
+ return null;
1447
+ }
1400
1448
 
1401
- return textContent;
1402
- };
1403
-
1404
- proto.getPath = function () {
1405
- return this._path;
1406
- };
1407
-
1408
- // --- Workaround for browsers that can't focus empty text nodes ---
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
+ }
1409
1462
 
1410
- // WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=15256
1463
+ function Squire ( doc, config ) {
1464
+ var win = doc.defaultView;
1465
+ var body = doc.body;
1466
+ var mutation;
1411
1467
 
1412
- var removeZWS = function ( root ) {
1413
- var walker = new TreeWalker( root, SHOW_TEXT, function () {
1414
- return true;
1415
- }, false ),
1416
- parent, node, index;
1417
- while ( node = walker.nextNode() ) {
1418
- while ( ( index = node.data.indexOf( ZWS ) ) > -1 ) {
1419
- if ( node.length === 1 ) {
1420
- do {
1421
- parent = node.parentNode;
1422
- parent.removeChild( node );
1423
- node = parent;
1424
- } while ( isInline( node ) && !getLength( node ) );
1425
- break;
1426
- } else {
1427
- node.deleteData( index, 1 );
1428
- }
1429
- }
1430
- }
1431
- };
1468
+ this._win = win;
1469
+ this._doc = doc;
1470
+ this._body = body;
1432
1471
 
1433
- proto._didAddZWS = function () {
1434
- this._hasZWS = true;
1435
- };
1436
- proto._removeZWS = function () {
1437
- if ( !this._hasZWS ) {
1438
- return;
1439
- }
1440
- removeZWS( this._body );
1441
- this._hasZWS = false;
1442
- };
1472
+ this._events = {};
1443
1473
 
1444
- // --- Path change events ---
1474
+ this._sel = win.getSelection();
1475
+ this._lastSelection = null;
1445
1476
 
1446
- proto._updatePath = function ( range, force ) {
1447
- var anchor = range.startContainer,
1448
- focus = range.endContainer,
1449
- newPath;
1450
- if ( force || anchor !== this._lastAnchorNode ||
1451
- focus !== this._lastFocusNode ) {
1452
- this._lastAnchorNode = anchor;
1453
- this._lastFocusNode = focus;
1454
- newPath = ( anchor && focus ) ? ( anchor === focus ) ?
1455
- getPath( focus ) : '(selection)' : '';
1456
- if ( this._path !== newPath ) {
1457
- this._path = newPath;
1458
- this.fireEvent( 'pathChange', { path: newPath } );
1459
- }
1460
- }
1461
- if ( !range.collapsed ) {
1462
- this.fireEvent( 'select' );
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 );
1463
1481
  }
1464
- };
1465
1482
 
1466
- proto._updatePathOnEvent = function () {
1467
- this._updatePath( this.getSelection() );
1468
- };
1483
+ this._hasZWS = false;
1469
1484
 
1470
- // --- Focus ---
1485
+ this._lastAnchorNode = null;
1486
+ this._lastFocusNode = null;
1487
+ this._path = '';
1471
1488
 
1472
- proto.focus = function () {
1473
- // FF seems to need the body to be focussed (at least on first load).
1474
- // Chrome also now needs body to be focussed in order to show the cursor
1475
- // (otherwise it is focussed, but the cursor doesn't appear).
1476
- // Opera (Presto-variant) however will lose the selection if you call this!
1477
- if ( !isPresto ) {
1478
- this._body.focus();
1479
- }
1480
- this._win.focus();
1481
- return this;
1482
- };
1489
+ this.addEventListener( 'keyup', this._updatePathOnEvent );
1490
+ this.addEventListener( 'mouseup', this._updatePathOnEvent );
1483
1491
 
1484
- proto.blur = function () {
1485
- // IE will remove the whole browser window from focus if you call
1486
- // win.blur() or body.blur(), so instead we call top.focus() to focus
1487
- // the top frame, thus blurring this frame. This works in everything
1488
- // except FF, so we need to call body.blur() in that as well.
1489
- if ( isGecko ) {
1490
- this._body.blur();
1492
+ win.addEventListener( 'focus', this, false );
1493
+ win.addEventListener( 'blur', this, false );
1494
+
1495
+ this._undoIndex = -1;
1496
+ this._undoStack = [];
1497
+ this._undoStackLength = 0;
1498
+ this._isInUndoState = false;
1499
+ this._ignoreChange = false;
1500
+
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 );
1491
1512
  }
1492
- top.focus();
1493
- return this;
1494
- };
1495
1513
 
1496
- // --- Bookmarking ---
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 );
1497
1519
 
1498
- var startSelectionId = 'squire-selection-start';
1499
- var endSelectionId = 'squire-selection-end';
1520
+ // Opera does not fire keydown repeatedly.
1521
+ this.addEventListener( isPresto ? 'keypress' : 'keydown', this._onKey );
1500
1522
 
1501
- proto._saveRangeToBookmark = function ( range ) {
1502
- var startNode = this.createElement( 'INPUT', {
1503
- id: startSelectionId,
1504
- type: 'hidden'
1505
- }),
1506
- endNode = this.createElement( 'INPUT', {
1507
- id: endSelectionId,
1508
- type: 'hidden'
1509
- }),
1510
- temp;
1523
+ // Add key handlers
1524
+ this._keyHandlers = Object.create( keyHandlers );
1511
1525
 
1512
- insertNodeInRange( range, startNode );
1513
- range.collapse( false );
1514
- insertNodeInRange( range, endNode );
1526
+ // Override default properties
1527
+ this.setConfig( config );
1515
1528
 
1516
- // In a collapsed range, the start is sometimes inserted after the end!
1517
- if ( startNode.compareDocumentPosition( endNode ) &
1518
- DOCUMENT_POSITION_PRECEDING ) {
1519
- startNode.id = endSelectionId;
1520
- endNode.id = startSelectionId;
1521
- temp = startNode;
1522
- startNode = endNode;
1523
- endNode = temp;
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 );
1550
+ }
1551
+ if ( toDelete ) {
1552
+ this.deleteData( offset, toDelete );
1553
+ }
1554
+ return afterSplit;
1555
+ };
1524
1556
  }
1525
1557
 
1526
- range.setStartAfter( startNode );
1527
- range.setEndBefore( endNode );
1528
- };
1558
+ body.setAttribute( 'contenteditable', 'true' );
1529
1559
 
1530
- proto._getRangeAndRemoveBookmark = function ( range ) {
1531
- var doc = this._doc,
1532
- start = doc.getElementById( startSelectionId ),
1533
- end = doc.getElementById( endSelectionId );
1560
+ // Remove Firefox's built-in controls
1561
+ try {
1562
+ doc.execCommand( 'enableObjectResizing', false, 'false' );
1563
+ doc.execCommand( 'enableInlineTableEditing', false, 'false' );
1564
+ } catch ( error ) {}
1534
1565
 
1535
- if ( start && end ) {
1536
- var startContainer = start.parentNode,
1537
- endContainer = end.parentNode,
1538
- collapsed;
1566
+ instances.push( this );
1539
1567
 
1540
- var _range = {
1541
- startContainer: startContainer,
1542
- endContainer: endContainer,
1543
- startOffset: indexOf.call( startContainer.childNodes, start ),
1544
- endOffset: indexOf.call( endContainer.childNodes, end )
1545
- };
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
+ }
1546
1572
 
1547
- if ( startContainer === endContainer ) {
1548
- _range.endOffset -= 1;
1573
+ var proto = Squire.prototype;
1574
+
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
1549
1584
  }
1585
+ }, config );
1550
1586
 
1551
- detach( start );
1552
- detach( end );
1587
+ // Users may specify block tag in lower case
1588
+ config.blockTag = config.blockTag.toUpperCase();
1553
1589
 
1554
- // Merge any text nodes we split
1555
- mergeInlines( startContainer, _range );
1556
- if ( startContainer !== endContainer ) {
1557
- mergeInlines( endContainer, _range );
1558
- }
1590
+ this._config = config;
1559
1591
 
1560
- if ( !range ) {
1561
- range = doc.createRange();
1562
- }
1563
- range.setStart( _range.startContainer, _range.startOffset );
1564
- range.setEnd( _range.endContainer, _range.endOffset );
1565
- collapsed = range.collapsed;
1592
+ return this;
1593
+ };
1566
1594
 
1567
- moveRangeBoundariesDownTree( range );
1568
- if ( collapsed ) {
1569
- range.collapse( true );
1570
- }
1571
- }
1572
- return range || null;
1595
+ proto.createElement = function ( tag, props, children ) {
1596
+ return createElement( this._doc, tag, props, children );
1573
1597
  };
1574
1598
 
1575
- // --- Undo ---
1599
+ proto.createDefaultBlock = function ( children ) {
1600
+ var config = this._config;
1601
+ return fixCursor(
1602
+ this.createElement( config.blockTag, config.blockAttributes, children )
1603
+ );
1604
+ };
1576
1605
 
1577
- proto._keyUpDetectChange = function ( event ) {
1578
- var code = event.keyCode;
1579
- // Presume document was changed if:
1580
- // 1. A modifier key (other than shift) wasn't held down
1581
- // 2. The key pressed is not in range 16<=x<=20 (control keys)
1582
- // 3. The key pressed is not in range 33<=x<=45 (navigation keys)
1583
- if ( !event.ctrlKey && !event.metaKey && !event.altKey &&
1584
- ( code < 16 || code > 20 ) &&
1585
- ( code < 33 || code > 45 ) ) {
1586
- this._docWasChanged();
1587
- }
1606
+ proto.didError = function ( error ) {
1607
+ console.log( error );
1588
1608
  };
1589
1609
 
1590
- proto._docWasChanged = function () {
1591
- if ( canObserveMutations && this._ignoreChange ) {
1592
- this._ignoreChange = false;
1593
- return;
1594
- }
1595
- if ( this._isInUndoState ) {
1596
- this._isInUndoState = false;
1597
- this.fireEvent( 'undoStateChange', {
1598
- canUndo: true,
1599
- canRedo: false
1600
- });
1601
- }
1602
- this.fireEvent( 'input' );
1610
+ proto.getDocument = function () {
1611
+ return this._doc;
1603
1612
  };
1604
1613
 
1605
- // Leaves bookmark
1606
- proto._recordUndoState = function ( range ) {
1607
- // Don't record if we're already in an undo state
1608
- if ( !this._isInUndoState ) {
1609
- // Advance pointer to new position
1610
- var undoIndex = this._undoIndex += 1,
1611
- undoStack = this._undoStack;
1614
+ // --- Events ---
1612
1615
 
1613
- // Truncate stack if longer (i.e. if has been previously undone)
1614
- if ( undoIndex < this._undoStackLength) {
1615
- undoStack.length = this._undoStackLength = undoIndex;
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
+ }
1616
1648
  }
1649
+ }
1650
+ return this;
1651
+ };
1617
1652
 
1618
- // Write out data
1619
- if ( range ) {
1620
- this._saveRangeToBookmark( range );
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();
1667
+ }
1668
+ var l = instances.length;
1669
+ while ( l-- ) {
1670
+ if ( instances[l] === this ) {
1671
+ instances.splice( l, 1 );
1621
1672
  }
1622
- undoStack[ undoIndex ] = this._getHTML();
1623
- this._undoStackLength += 1;
1624
- this._isInUndoState = true;
1625
1673
  }
1626
1674
  };
1627
1675
 
1628
- proto.undo = function () {
1629
- // Sanity check: must not be at beginning of the history stack
1630
- if ( this._undoIndex !== 0 || !this._isInUndoState ) {
1631
- // Make sure any changes since last checkpoint are saved.
1632
- this._recordUndoState( this.getSelection() );
1676
+ proto.handleEvent = function ( event ) {
1677
+ this.fireEvent( event.type, event );
1678
+ };
1633
1679
 
1634
- this._undoIndex -= 1;
1635
- this._setHTML( this._undoStack[ this._undoIndex ] );
1636
- var range = this._getRangeAndRemoveBookmark();
1637
- if ( range ) {
1638
- this.setSelection( range );
1639
- }
1640
- this._isInUndoState = true;
1641
- this.fireEvent( 'undoStateChange', {
1642
- canUndo: this._undoIndex !== 0,
1643
- canRedo: true
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
1644
1686
  });
1645
- this.fireEvent( 'input' );
1687
+ return this;
1688
+ }
1689
+ if ( !handlers ) {
1690
+ handlers = this._events[ type ] = [];
1691
+ if ( !customEvents[ type ] ) {
1692
+ this._doc.addEventListener( type, this, true );
1693
+ }
1646
1694
  }
1695
+ handlers.push( fn );
1647
1696
  return this;
1648
1697
  };
1649
1698
 
1650
- proto.redo = function () {
1651
- // Sanity check: must not be at end of stack and must be in an undo
1652
- // state.
1653
- var undoIndex = this._undoIndex,
1654
- undoStackLength = this._undoStackLength;
1655
- if ( undoIndex + 1 < undoStackLength && this._isInUndoState ) {
1656
- this._undoIndex += 1;
1657
- this._setHTML( this._undoStack[ this._undoIndex ] );
1658
- var range = this._getRangeAndRemoveBookmark();
1659
- if ( range ) {
1660
- this.setSelection( range );
1699
+ proto.removeEventListener = function ( type, fn ) {
1700
+ var handlers = this._events[ type ],
1701
+ l;
1702
+ if ( handlers ) {
1703
+ l = handlers.length;
1704
+ while ( l-- ) {
1705
+ if ( handlers[l] === fn ) {
1706
+ handlers.splice( l, 1 );
1707
+ }
1708
+ }
1709
+ if ( !handlers.length ) {
1710
+ delete this._events[ type ];
1711
+ if ( !customEvents[ type ] ) {
1712
+ this._doc.removeEventListener( type, this, false );
1713
+ }
1661
1714
  }
1662
- this.fireEvent( 'undoStateChange', {
1663
- canUndo: true,
1664
- canRedo: undoIndex + 2 < undoStackLength
1665
- });
1666
- this.fireEvent( 'input' );
1667
1715
  }
1668
1716
  return this;
1669
1717
  };
1670
1718
 
1671
- // --- Inline formatting ---
1719
+ // --- Selection and Path ---
1672
1720
 
1673
- // Looks for matching tag and attributes, so won't work
1674
- // if <strong> instead of <b> etc.
1675
- proto.hasFormat = function ( tag, attributes, range ) {
1676
- // 1. Normalise the arguments and get selection
1677
- tag = tag.toUpperCase();
1678
- if ( !attributes ) { attributes = {}; }
1679
- if ( !range && !( range = this.getSelection() ) ) {
1680
- return false;
1721
+ proto._createRange =
1722
+ function ( range, startOffset, endContainer, endOffset ) {
1723
+ if ( range instanceof this._win.Range ) {
1724
+ return range.cloneRange();
1681
1725
  }
1682
-
1683
- // If the common ancestor is inside the tag we require, we definitely
1684
- // have the format.
1685
- var root = range.commonAncestorContainer,
1686
- walker, node;
1687
- if ( getNearest( root, tag, attributes ) ) {
1688
- return true;
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 );
1689
1732
  }
1733
+ return domRange;
1734
+ };
1690
1735
 
1691
- // If common ancestor is a text node and doesn't have the format, we
1692
- // definitely don't have it.
1693
- if ( root.nodeType === TEXT_NODE ) {
1694
- return false;
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();
1744
+ }
1745
+ var sel = this._sel;
1746
+ sel.removeAllRanges();
1747
+ sel.addRange( range );
1695
1748
  }
1749
+ return this;
1750
+ };
1696
1751
 
1697
- // Otherwise, check each text node at least partially contained within
1698
- // the selection and make sure all of them have the format we want.
1699
- walker = new TreeWalker( root, SHOW_TEXT, function ( node ) {
1700
- return isNodeContainedInRange( range, node, true );
1701
- }, false );
1702
-
1703
- var seenNode = false;
1704
- while ( node = walker.nextNode() ) {
1705
- if ( !getNearest( node, tag, attributes ) ) {
1706
- return false;
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 );
1707
1762
  }
1708
- seenNode = true;
1763
+ if ( endContainer && isLeaf( endContainer ) ) {
1764
+ selection.setEndBefore( endContainer );
1765
+ }
1766
+ this._lastSelection = selection;
1767
+ } else {
1768
+ selection = this._lastSelection;
1709
1769
  }
1710
-
1711
- return seenNode;
1770
+ if ( !selection ) {
1771
+ selection = this._createRange( this._body.firstChild, 0 );
1772
+ }
1773
+ return selection;
1712
1774
  };
1713
1775
 
1714
- proto._addFormat = function ( tag, attributes, range ) {
1715
- // If the range is collapsed we simply insert the node by wrapping
1716
- // it round the range and focus it.
1717
- var el, walker, startContainer, endContainer, startOffset, endOffset,
1718
- node, needsFormat;
1719
-
1720
- if ( range.collapsed ) {
1721
- el = fixCursor( this.createElement( tag, attributes ) );
1722
- insertNodeInRange( range, el );
1723
- range.setStart( el.firstChild, el.firstChild.length );
1724
- range.collapse( true );
1725
- }
1726
- // Otherwise we find all the textnodes in the range (splitting
1727
- // partially selected nodes) and if they're not already formatted
1728
- // correctly we wrap them in the appropriate tag.
1729
- else {
1730
- // Create an iterator to walk over all the text nodes under this
1731
- // ancestor which are in the range and not already formatted
1732
- // correctly.
1733
- //
1734
- // In Blink/WebKit, empty blocks may have no text nodes, just a <br>.
1735
- // Therefore we wrap this in the tag as well, as this will then cause it
1736
- // to apply when the user types something in the block, which is
1737
- // presumably what was intended.
1776
+ proto.getSelectedText = function () {
1777
+ var range = this.getSelection(),
1738
1778
  walker = new TreeWalker(
1739
1779
  range.commonAncestorContainer,
1740
1780
  SHOW_TEXT|SHOW_ELEMENT,
1741
1781
  function ( node ) {
1742
- return ( node.nodeType === TEXT_NODE ||
1743
- node.nodeName === 'BR' ) &&
1744
- isNodeContainedInRange( range, node, true );
1745
- },
1746
- false
1747
- );
1748
-
1749
- // Start at the beginning node of the range and iterate through
1750
- // all the nodes in the range that need formatting.
1751
- startContainer = range.startContainer;
1752
- startOffset = range.startOffset;
1753
- endContainer = range.endContainer;
1754
- endOffset = range.endOffset;
1755
-
1756
- // Make sure we start with a valid node.
1757
- walker.currentNode = startContainer;
1758
- if ( !walker.filter( startContainer ) ) {
1759
- startContainer = walker.nextNode();
1760
- startOffset = 0;
1761
- }
1782
+ return isNodeContainedInRange( range, node, true );
1783
+ }
1784
+ ),
1785
+ startContainer = range.startContainer,
1786
+ endContainer = range.endContainer,
1787
+ node = walker.currentNode = startContainer,
1788
+ textContent = '',
1789
+ addedTextInBlock = false,
1790
+ value;
1762
1791
 
1763
- // If there are no interesting nodes in the selection, abort
1764
- if ( !startContainer ) {
1765
- return range;
1766
- }
1792
+ if ( !walker.filter( node ) ) {
1793
+ node = walker.nextNode();
1794
+ }
1767
1795
 
1768
- do {
1769
- node = walker.currentNode;
1770
- needsFormat = !getNearest( node, tag, attributes );
1771
- if ( needsFormat ) {
1772
- // <br> can never be a container node, so must have a text node
1773
- // if node == (end|start)Container
1774
- if ( node === endContainer && node.length > endOffset ) {
1775
- node.splitText( endOffset );
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 );
1776
1802
  }
1777
- if ( node === startContainer && startOffset ) {
1778
- node = node.splitText( startOffset );
1779
- if ( endContainer === startContainer ) {
1780
- endContainer = node;
1781
- endOffset -= startOffset;
1782
- }
1783
- startContainer = node;
1784
- startOffset = 0;
1803
+ if ( node === startContainer ) {
1804
+ value = value.slice( range.startOffset );
1785
1805
  }
1786
- el = this.createElement( tag, attributes );
1787
- replaceWith( node, el );
1788
- el.appendChild( node );
1806
+ textContent += value;
1807
+ addedTextInBlock = true;
1789
1808
  }
1790
- } while ( walker.nextNode() );
1809
+ } else if ( node.nodeName === 'BR' ||
1810
+ addedTextInBlock && !isInline( node ) ) {
1811
+ textContent += '\n';
1812
+ addedTextInBlock = false;
1813
+ }
1814
+ node = walker.nextNode();
1815
+ }
1791
1816
 
1792
- // If we don't finish inside a text node, offset may have changed.
1793
- if ( endContainer.nodeType !== TEXT_NODE ) {
1794
- if ( node.nodeType === TEXT_NODE ) {
1795
- endContainer = node;
1796
- endOffset = node.length;
1817
+ return textContent;
1818
+ };
1819
+
1820
+ proto.getPath = function () {
1821
+ return this._path;
1822
+ };
1823
+
1824
+ // --- Workaround for browsers that can't focus empty text nodes ---
1825
+
1826
+ // WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=15256
1827
+
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;
1797
1842
  } else {
1798
- // If <br>, we must have just wrapped it, so it must have only
1799
- // one child
1800
- endContainer = node.parentNode;
1801
- endOffset = 1;
1843
+ node.deleteData( index, 1 );
1802
1844
  }
1803
1845
  }
1846
+ }
1847
+ };
1804
1848
 
1805
- // Now set the selection to as it was before
1806
- range = this._createRange(
1807
- startContainer, startOffset, endContainer, endOffset );
1849
+ proto._didAddZWS = function () {
1850
+ this._hasZWS = true;
1851
+ };
1852
+ proto._removeZWS = function () {
1853
+ if ( !this._hasZWS ) {
1854
+ return;
1808
1855
  }
1809
- return range;
1856
+ removeZWS( this._body );
1857
+ this._hasZWS = false;
1810
1858
  };
1811
1859
 
1812
- proto._removeFormat = function ( tag, attributes, range, partial ) {
1813
- // Add bookmark
1814
- this._saveRangeToBookmark( range );
1860
+ // --- Path change events ---
1815
1861
 
1816
- // We need a node in the selection to break the surrounding
1817
- // formatted text.
1818
- var doc = this._doc,
1819
- fixer;
1820
- if ( range.collapsed ) {
1821
- if ( cantFocusEmptyTextNodes ) {
1822
- fixer = doc.createTextNode( ZWS );
1823
- this._didAddZWS();
1824
- } else {
1825
- fixer = doc.createTextNode( '' );
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 } );
1826
1875
  }
1827
- insertNodeInRange( range, fixer );
1828
1876
  }
1829
-
1830
- // Find block-level ancestor of selection
1831
- var root = range.commonAncestorContainer;
1832
- while ( isInline( root ) ) {
1833
- root = root.parentNode;
1877
+ if ( !range.collapsed ) {
1878
+ this.fireEvent( 'select' );
1834
1879
  }
1880
+ };
1835
1881
 
1836
- // Find text nodes inside formatTags that are not in selection and
1837
- // add an extra tag with the same formatting.
1838
- var startContainer = range.startContainer,
1839
- startOffset = range.startOffset,
1840
- endContainer = range.endContainer,
1841
- endOffset = range.endOffset,
1842
- toWrap = [],
1843
- examineNode = function ( node, exemplar ) {
1844
- // If the node is completely contained by the range then
1845
- // we're going to remove all formatting so ignore it.
1846
- if ( isNodeContainedInRange( range, node, false ) ) {
1847
- return;
1848
- }
1849
-
1850
- var isText = ( node.nodeType === TEXT_NODE ),
1851
- child, next;
1882
+ proto._updatePathOnEvent = function () {
1883
+ this._updatePath( this.getSelection() );
1884
+ };
1852
1885
 
1853
- // If not at least partially contained, wrap entire contents
1854
- // in a clone of the tag we're removing and we're done.
1855
- if ( !isNodeContainedInRange( range, node, true ) ) {
1856
- // Ignore bookmarks and empty text nodes
1857
- if ( node.nodeName !== 'INPUT' &&
1858
- ( !isText || node.data ) ) {
1859
- toWrap.push([ exemplar, node ]);
1860
- }
1861
- return;
1862
- }
1886
+ // --- Focus ---
1863
1887
 
1864
- // Split any partially selected text nodes.
1865
- if ( isText ) {
1866
- if ( node === endContainer && endOffset !== node.length ) {
1867
- toWrap.push([ exemplar, node.splitText( endOffset ) ]);
1868
- }
1869
- if ( node === startContainer && startOffset ) {
1870
- node.splitText( startOffset );
1871
- toWrap.push([ exemplar, node ]);
1872
- }
1873
- }
1874
- // If not a text node, recurse onto all children.
1875
- // Beware, the tree may be rewritten with each call
1876
- // to examineNode, hence find the next sibling first.
1877
- else {
1878
- for ( child = node.firstChild; child; child = next ) {
1879
- next = child.nextSibling;
1880
- examineNode( child, exemplar );
1881
- }
1882
- }
1883
- },
1884
- formatTags = Array.prototype.filter.call(
1885
- root.getElementsByTagName( tag ), function ( el ) {
1886
- return isNodeContainedInRange( range, el, true ) &&
1887
- hasTagAttributes( el, tag, attributes );
1888
- }
1889
- );
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();
1895
+ }
1896
+ this._win.focus();
1897
+ return this;
1898
+ };
1890
1899
 
1891
- if ( !partial ) {
1892
- formatTags.forEach( function ( node ) {
1893
- examineNode( node, node );
1894
- });
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();
1895
1907
  }
1908
+ top.focus();
1909
+ return this;
1910
+ };
1896
1911
 
1897
- // Now wrap unselected nodes in the tag
1898
- toWrap.forEach( function ( item ) {
1899
- // [ exemplar, node ] tuple
1900
- var el = item[0].cloneNode( false ),
1901
- node = item[1];
1902
- replaceWith( node, el );
1903
- el.appendChild( node );
1904
- });
1905
- // and remove old formatting tags.
1906
- formatTags.forEach( function ( el ) {
1907
- replaceWith( el, empty( el ) );
1908
- });
1912
+ // --- Bookmarking ---
1909
1913
 
1910
- // Merge adjacent inlines:
1911
- this._getRangeAndRemoveBookmark( range );
1912
- if ( fixer ) {
1913
- range.collapse( false );
1914
+ var startSelectionId = 'squire-selection-start';
1915
+ var endSelectionId = 'squire-selection-end';
1916
+
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;
1927
+
1928
+ insertNodeInRange( range, startNode );
1929
+ range.collapse( false );
1930
+ insertNodeInRange( range, endNode );
1931
+
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;
1914
1940
  }
1915
- var _range = {
1916
- startContainer: range.startContainer,
1917
- startOffset: range.startOffset,
1918
- endContainer: range.endContainer,
1919
- endOffset: range.endOffset
1920
- };
1921
- mergeInlines( root, _range );
1922
- range.setStart( _range.startContainer, _range.startOffset );
1923
- range.setEnd( _range.endContainer, _range.endOffset );
1924
1941
 
1925
- return range;
1942
+ range.setStartAfter( startNode );
1943
+ range.setEndBefore( endNode );
1926
1944
  };
1927
1945
 
1928
- proto.changeFormat = function ( add, remove, range, partial ) {
1929
- // Normalise the arguments and get selection
1930
- if ( !range && !( range = this.getSelection() ) ) {
1931
- return;
1932
- }
1946
+ proto._getRangeAndRemoveBookmark = function ( range ) {
1947
+ var doc = this._doc,
1948
+ start = doc.getElementById( startSelectionId ),
1949
+ end = doc.getElementById( endSelectionId );
1933
1950
 
1934
- // Save undo checkpoint
1935
- this._recordUndoState( range );
1936
- this._getRangeAndRemoveBookmark( range );
1951
+ if ( start && end ) {
1952
+ var startContainer = start.parentNode,
1953
+ endContainer = end.parentNode,
1954
+ collapsed;
1955
+
1956
+ var _range = {
1957
+ startContainer: startContainer,
1958
+ endContainer: endContainer,
1959
+ startOffset: indexOf.call( startContainer.childNodes, start ),
1960
+ endOffset: indexOf.call( endContainer.childNodes, end )
1961
+ };
1962
+
1963
+ if ( startContainer === endContainer ) {
1964
+ _range.endOffset -= 1;
1965
+ }
1966
+
1967
+ detach( start );
1968
+ detach( end );
1969
+
1970
+ // Merge any text nodes we split
1971
+ mergeInlines( startContainer, _range );
1972
+ if ( startContainer !== endContainer ) {
1973
+ mergeInlines( endContainer, _range );
1974
+ }
1937
1975
 
1938
- if ( remove ) {
1939
- range = this._removeFormat( remove.tag.toUpperCase(),
1940
- remove.attributes || {}, range, partial );
1941
- }
1942
- if ( add ) {
1943
- range = this._addFormat( add.tag.toUpperCase(),
1944
- add.attributes || {}, range );
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;
1982
+
1983
+ moveRangeBoundariesDownTree( range );
1984
+ if ( collapsed ) {
1985
+ range.collapse( true );
1986
+ }
1945
1987
  }
1988
+ return range || null;
1989
+ };
1946
1990
 
1947
- this.setSelection( range );
1948
- this._updatePath( range, true );
1991
+ // --- Undo ---
1949
1992
 
1950
- // We're not still in an undo state
1951
- if ( !canObserveMutations ) {
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 ) ) {
1952
2002
  this._docWasChanged();
1953
2003
  }
2004
+ };
1954
2005
 
1955
- return this;
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' );
1956
2019
  };
1957
2020
 
1958
- // --- Block formatting ---
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;
1959
2028
 
1960
- var tagAfterSplit = {
1961
- DT: 'DD',
1962
- DD: 'DT',
1963
- LI: 'LI'
2029
+ // Truncate stack if longer (i.e. if has been previously undone)
2030
+ if ( undoIndex < this._undoStackLength) {
2031
+ undoStack.length = this._undoStackLength = undoIndex;
2032
+ }
2033
+
2034
+ // Write out data
2035
+ if ( range ) {
2036
+ this._saveRangeToBookmark( range );
2037
+ }
2038
+ undoStack[ undoIndex ] = this._getHTML();
2039
+ this._undoStackLength += 1;
2040
+ this._isInUndoState = true;
2041
+ }
1964
2042
  };
1965
2043
 
1966
- var splitBlock = function ( self, block, node, offset ) {
1967
- var splitTag = tagAfterSplit[ block.nodeName ],
1968
- splitProperties = null,
1969
- nodeAfterSplit = split( node, offset, block.parentNode );
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() );
1970
2049
 
1971
- if ( !splitTag ) {
1972
- splitTag = self.defaultBlockTag;
1973
- splitProperties = self.defaultBlockProperties;
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
2060
+ });
2061
+ this.fireEvent( 'input' );
1974
2062
  }
2063
+ return this;
2064
+ };
1975
2065
 
1976
- // Make sure the new node is the correct type.
1977
- if ( !hasTagAttributes( nodeAfterSplit, splitTag, splitProperties ) ) {
1978
- block = createElement( nodeAfterSplit.ownerDocument,
1979
- splitTag, splitProperties );
1980
- if ( nodeAfterSplit.dir ) {
1981
- block.className = nodeAfterSplit.dir === 'rtl' ? 'dir-rtl' : '';
1982
- block.dir = nodeAfterSplit.dir;
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 );
1983
2077
  }
1984
- replaceWith( nodeAfterSplit, block );
1985
- block.appendChild( empty( nodeAfterSplit ) );
1986
- nodeAfterSplit = block;
2078
+ this.fireEvent( 'undoStateChange', {
2079
+ canUndo: true,
2080
+ canRedo: undoIndex + 2 < undoStackLength
2081
+ });
2082
+ this.fireEvent( 'input' );
1987
2083
  }
1988
- return nodeAfterSplit;
2084
+ return this;
1989
2085
  };
1990
2086
 
1991
- proto.forEachBlock = function ( fn, mutates, range ) {
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 = {}; }
1992
2095
  if ( !range && !( range = this.getSelection() ) ) {
1993
- return this;
2096
+ return false;
1994
2097
  }
1995
2098
 
1996
- // Save undo checkpoint
1997
- if ( mutates ) {
1998
- this._recordUndoState( range );
1999
- this._getRangeAndRemoveBookmark( range );
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;
2000
2105
  }
2001
2106
 
2002
- var start = getStartBlockOfRange( range ),
2003
- end = getEndBlockOfRange( range );
2004
- if ( start && end ) {
2005
- do {
2006
- if ( fn( start ) || start === end ) { break; }
2007
- } while ( start = getNextBlock( start ) );
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;
2008
2111
  }
2009
2112
 
2010
- if ( mutates ) {
2011
- this.setSelection( range );
2012
-
2013
- // Path may have changed
2014
- this._updatePath( range, true );
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 );
2015
2118
 
2016
- // We're not still in an undo state
2017
- if ( !canObserveMutations ) {
2018
- this._docWasChanged();
2119
+ var seenNode = false;
2120
+ while ( node = walker.nextNode() ) {
2121
+ if ( !getNearest( node, tag, attributes ) ) {
2122
+ return false;
2019
2123
  }
2124
+ seenNode = true;
2020
2125
  }
2021
- return this;
2126
+
2127
+ return seenNode;
2022
2128
  };
2023
2129
 
2024
- proto.modifyBlocks = function ( modify, range ) {
2025
- if ( !range && !( range = this.getSelection() ) ) {
2026
- return this;
2027
- }
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;
2028
2135
 
2029
- // 1. Save undo checkpoint and bookmark selection
2030
- if ( this._isInUndoState ) {
2031
- this._saveRangeToBookmark( range );
2032
- } else {
2033
- this._recordUndoState( range );
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 );
2034
2141
  }
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.
2154
+ walker = new TreeWalker(
2155
+ range.commonAncestorContainer,
2156
+ SHOW_TEXT|SHOW_ELEMENT,
2157
+ function ( node ) {
2158
+ return ( node.nodeType === TEXT_NODE ||
2159
+ node.nodeName === 'BR' ) &&
2160
+ isNodeContainedInRange( range, node, true );
2161
+ },
2162
+ false
2163
+ );
2035
2164
 
2036
- // 2. Expand range to block boundaries
2037
- expandRangeToBlockBoundaries( range );
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;
2038
2171
 
2039
- // 3. Remove range.
2040
- var body = this._body,
2041
- frag;
2042
- moveRangeBoundariesUpTree( range, body );
2043
- frag = extractContentsOfRange( range, body );
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
+ }
2178
+
2179
+ // If there are no interesting nodes in the selection, abort
2180
+ if ( !startContainer ) {
2181
+ return range;
2182
+ }
2183
+
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 );
2192
+ }
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;
2201
+ }
2202
+ el = this.createElement( tag, attributes );
2203
+ replaceWith( node, el );
2204
+ el.appendChild( node );
2205
+ }
2206
+ } while ( walker.nextNode() );
2044
2207
 
2045
- // 4. Modify tree of fragment and reinsert.
2046
- insertNodeInRange( range, modify.call( this, frag ) );
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;
2213
+ } 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;
2218
+ }
2219
+ }
2047
2220
 
2048
- // 5. Merge containers at edges
2049
- if ( range.endOffset < range.endContainer.childNodes.length ) {
2050
- mergeContainers( range.endContainer.childNodes[ range.endOffset ] );
2221
+ // Now set the selection to as it was before
2222
+ range = this._createRange(
2223
+ startContainer, startOffset, endContainer, endOffset );
2051
2224
  }
2052
- mergeContainers( range.startContainer.childNodes[ range.startOffset ] );
2225
+ return range;
2226
+ };
2053
2227
 
2054
- // 6. Restore selection
2055
- this._getRangeAndRemoveBookmark( range );
2056
- this.setSelection( range );
2057
- this._updatePath( range, true );
2228
+ proto._removeFormat = function ( tag, attributes, range, partial ) {
2229
+ // Add bookmark
2230
+ this._saveRangeToBookmark( range );
2058
2231
 
2059
- // 7. We're not still in an undo state
2060
- if ( !canObserveMutations ) {
2061
- this._docWasChanged();
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( '' );
2242
+ }
2243
+ insertNodeInRange( range, fixer );
2062
2244
  }
2063
2245
 
2064
- return this;
2065
- };
2066
-
2067
- var increaseBlockQuoteLevel = function ( frag ) {
2068
- return this.createElement( 'BLOCKQUOTE', [
2069
- frag
2070
- ]);
2071
- };
2246
+ // Find block-level ancestor of selection
2247
+ var root = range.commonAncestorContainer;
2248
+ while ( isInline( root ) ) {
2249
+ root = root.parentNode;
2250
+ }
2072
2251
 
2073
- var decreaseBlockQuoteLevel = function ( frag ) {
2074
- var blockquotes = frag.querySelectorAll( 'blockquote' );
2075
- Array.prototype.filter.call( blockquotes, function ( el ) {
2076
- return !getNearest( el.parentNode, 'BLOCKQUOTE' );
2077
- }).forEach( function ( el ) {
2078
- replaceWith( el, empty( el ) );
2079
- });
2080
- return frag;
2081
- };
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
+ }
2082
2265
 
2083
- var removeBlockQuote = function (/* frag */) {
2084
- return this.createDefaultBlock([
2085
- this.createElement( 'INPUT', {
2086
- id: startSelectionId,
2087
- type: 'hidden'
2088
- }),
2089
- this.createElement( 'INPUT', {
2090
- id: endSelectionId,
2091
- type: 'hidden'
2092
- })
2093
- ]);
2094
- };
2266
+ var isText = ( node.nodeType === TEXT_NODE ),
2267
+ child, next;
2095
2268
 
2096
- var makeList = function ( self, frag, type ) {
2097
- var walker = getBlockWalker( frag ),
2098
- node, tag, prev, newLi;
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
+ }
2099
2279
 
2100
- while ( node = walker.nextNode() ) {
2101
- tag = node.parentNode.nodeName;
2102
- if ( tag !== 'LI' ) {
2103
- newLi = self.createElement( 'LI', {
2104
- 'class': node.dir === 'rtl' ? 'dir-rtl' : undefined,
2105
- dir: node.dir || undefined
2106
- });
2107
- // Have we replaced the previous block with a new <ul>/<ol>?
2108
- if ( ( prev = node.previousSibling ) &&
2109
- prev.nodeName === type ) {
2110
- prev.appendChild( newLi );
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
+ }
2111
2289
  }
2112
- // Otherwise, replace this block with the <ul>/<ol>
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.
2113
2293
  else {
2114
- replaceWith(
2115
- node,
2116
- self.createElement( type, [
2117
- newLi
2118
- ])
2119
- );
2294
+ for ( child = node.firstChild; child; child = next ) {
2295
+ next = child.nextSibling;
2296
+ examineNode( child, exemplar );
2297
+ }
2120
2298
  }
2121
- newLi.appendChild( node );
2122
- } else {
2123
- node = node.parentNode.parentNode;
2124
- tag = node.nodeName;
2125
- if ( tag !== type && ( /^[OU]L$/.test( tag ) ) ) {
2126
- replaceWith( node,
2127
- self.createElement( type, [ empty( node ) ] )
2128
- );
2299
+ },
2300
+ formatTags = Array.prototype.filter.call(
2301
+ root.getElementsByTagName( tag ), function ( el ) {
2302
+ return isNodeContainedInRange( range, el, true ) &&
2303
+ hasTagAttributes( el, tag, attributes );
2129
2304
  }
2130
- }
2131
- }
2132
- };
2133
-
2134
- var makeUnorderedList = function ( frag ) {
2135
- makeList( this, frag, 'UL' );
2136
- return frag;
2137
- };
2138
-
2139
- var makeOrderedList = function ( frag ) {
2140
- makeList( this, frag, 'OL' );
2141
- return frag;
2142
- };
2305
+ );
2143
2306
 
2144
- var removeList = function ( frag ) {
2145
- var lists = frag.querySelectorAll( 'UL, OL' ),
2146
- i, l, ll, list, listFrag, children, child;
2147
- for ( i = 0, l = lists.length; i < l; i += 1 ) {
2148
- list = lists[i];
2149
- listFrag = empty( list );
2150
- children = listFrag.childNodes;
2151
- ll = children.length;
2152
- while ( ll-- ) {
2153
- child = children[ll];
2154
- replaceWith( child, empty( child ) );
2155
- }
2156
- fixContainer( listFrag );
2157
- replaceWith( list, listFrag );
2307
+ if ( !partial ) {
2308
+ formatTags.forEach( function ( node ) {
2309
+ examineNode( node, node );
2310
+ });
2158
2311
  }
2159
- return frag;
2160
- };
2161
2312
 
2162
- var increaseListLevel = function ( frag ) {
2163
- var items = frag.querySelectorAll( 'LI' ),
2164
- i, l, item,
2165
- type, newParent;
2166
- for ( i = 0, l = items.length; i < l; i += 1 ) {
2167
- item = items[i];
2168
- if ( !isContainer( item.firstChild ) ) {
2169
- // type => 'UL' or 'OL'
2170
- type = item.parentNode.nodeName;
2171
- newParent = item.previousSibling;
2172
- if ( !newParent || !( newParent = newParent.lastChild ) ||
2173
- newParent.nodeName !== type ) {
2174
- replaceWith(
2175
- item,
2176
- this.createElement( 'LI', [
2177
- newParent = this.createElement( type )
2178
- ])
2179
- );
2180
- }
2181
- newParent.appendChild( item );
2182
- }
2183
- }
2184
- return frag;
2185
- };
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
+ });
2186
2325
 
2187
- var decreaseListLevel = function ( frag ) {
2188
- var items = frag.querySelectorAll( 'LI' );
2189
- Array.prototype.filter.call( items, function ( el ) {
2190
- return !isContainer( el.firstChild );
2191
- }).forEach( function ( item ) {
2192
- var parent = item.parentNode,
2193
- newParent = parent.parentNode,
2194
- first = item.firstChild,
2195
- node = first,
2196
- next;
2197
- if ( item.previousSibling ) {
2198
- parent = split( parent, item, newParent );
2199
- }
2200
- while ( node ) {
2201
- next = node.nextSibling;
2202
- if ( isContainer( node ) ) {
2203
- break;
2204
- }
2205
- newParent.insertBefore( node, parent );
2206
- node = next;
2207
- }
2208
- if ( newParent.nodeName === 'LI' && first.previousSibling ) {
2209
- split( newParent, first, newParent.parentNode );
2210
- }
2211
- while ( item !== frag && !item.childNodes.length ) {
2212
- parent = item.parentNode;
2213
- parent.removeChild( item );
2214
- item = parent;
2215
- }
2216
- }, this );
2217
- fixContainer( frag );
2218
- return frag;
2326
+ // Merge adjacent inlines:
2327
+ this._getRangeAndRemoveBookmark( range );
2328
+ if ( fixer ) {
2329
+ range.collapse( false );
2330
+ }
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
+
2341
+ return range;
2219
2342
  };
2220
2343
 
2221
- // --- Clean ---
2344
+ proto.changeFormat = function ( add, remove, range, partial ) {
2345
+ // Normalise the arguments and get selection
2346
+ if ( !range && !( range = this.getSelection() ) ) {
2347
+ return;
2348
+ }
2222
2349
 
2223
- 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;
2350
+ // Save undo checkpoint
2351
+ this._recordUndoState( range );
2352
+ this._getRangeAndRemoveBookmark( range );
2224
2353
 
2225
- var addLinks = function ( frag ) {
2226
- var doc = frag.ownerDocument,
2227
- walker = new TreeWalker( frag, SHOW_TEXT,
2228
- function ( node ) {
2229
- return !getNearest( node, 'A' );
2230
- }, false ),
2231
- node, data, parent, match, index, endIndex, child;
2232
- while ( node = walker.nextNode() ) {
2233
- data = node.data;
2234
- parent = node.parentNode;
2235
- while ( match = linkRegExp.exec( data ) ) {
2236
- index = match.index;
2237
- endIndex = index + match[0].length;
2238
- if ( index ) {
2239
- child = doc.createTextNode( data.slice( 0, index ) );
2240
- parent.insertBefore( child, node );
2241
- }
2242
- child = doc.createElement( 'A' );
2243
- child.textContent = data.slice( index, endIndex );
2244
- child.href = match[1] ?
2245
- /^(?:ht|f)tps?:/.test( match[1] ) ?
2246
- match[1] :
2247
- 'http://' + match[1] :
2248
- 'mailto:' + match[2];
2249
- parent.insertBefore( child, node );
2250
- node.data = data = data.slice( endIndex );
2251
- }
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 );
2252
2361
  }
2253
- };
2254
-
2255
- 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)$/;
2256
2362
 
2257
- var fontSizes = {
2258
- 1: 10,
2259
- 2: 13,
2260
- 3: 16,
2261
- 4: 18,
2262
- 5: 24,
2263
- 6: 32,
2264
- 7: 48
2265
- };
2363
+ this.setSelection( range );
2364
+ this._updatePath( range, true );
2266
2365
 
2267
- var spanToSemantic = {
2268
- backgroundColor: {
2269
- regexp: notWS,
2270
- replace: function ( doc, colour ) {
2271
- return createElement( doc, 'SPAN', {
2272
- 'class': 'highlight',
2273
- style: 'background-color: ' + colour
2274
- });
2275
- }
2276
- },
2277
- color: {
2278
- regexp: notWS,
2279
- replace: function ( doc, colour ) {
2280
- return createElement( doc, 'SPAN', {
2281
- 'class': 'colour',
2282
- style: 'color:' + colour
2283
- });
2284
- }
2285
- },
2286
- fontWeight: {
2287
- regexp: /^bold/i,
2288
- replace: function ( doc ) {
2289
- return createElement( doc, 'B' );
2290
- }
2291
- },
2292
- fontStyle: {
2293
- regexp: /^italic/i,
2294
- replace: function ( doc ) {
2295
- return createElement( doc, 'I' );
2296
- }
2297
- },
2298
- fontFamily: {
2299
- regexp: notWS,
2300
- replace: function ( doc, family ) {
2301
- return createElement( doc, 'SPAN', {
2302
- 'class': 'font',
2303
- style: 'font-family:' + family
2304
- });
2305
- }
2306
- },
2307
- fontSize: {
2308
- regexp: notWS,
2309
- replace: function ( doc, size ) {
2310
- return createElement( doc, 'SPAN', {
2311
- 'class': 'size',
2312
- style: 'font-size:' + size
2313
- });
2314
- }
2366
+ // We're not still in an undo state
2367
+ if ( !canObserveMutations ) {
2368
+ this._docWasChanged();
2315
2369
  }
2370
+
2371
+ return this;
2316
2372
  };
2317
2373
 
2318
- var replaceWithTag = function ( tag ) {
2319
- return function ( node, parent ) {
2320
- var el = createElement( node.ownerDocument, tag );
2321
- parent.replaceChild( el, node );
2322
- el.appendChild( empty( node ) );
2323
- return el;
2324
- };
2374
+ // --- Block formatting ---
2375
+
2376
+ var tagAfterSplit = {
2377
+ DT: 'DD',
2378
+ DD: 'DT',
2379
+ LI: 'LI'
2325
2380
  };
2326
2381
 
2327
- var stylesRewriters = {
2328
- SPAN: function ( span, parent ) {
2329
- var style = span.style,
2330
- doc = span.ownerDocument,
2331
- attr, converter, css, newTreeBottom, newTreeTop, el;
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;
2332
2387
 
2333
- for ( attr in spanToSemantic ) {
2334
- converter = spanToSemantic[ attr ];
2335
- css = style[ attr ];
2336
- if ( css && converter.regexp.test( css ) ) {
2337
- el = converter.replace( doc, css );
2338
- if ( newTreeBottom ) {
2339
- newTreeBottom.appendChild( el );
2340
- }
2341
- newTreeBottom = el;
2342
- if ( !newTreeTop ) {
2343
- newTreeTop = el;
2344
- }
2345
- }
2346
- }
2388
+ if ( !splitTag ) {
2389
+ splitTag = config.blockTag;
2390
+ splitProperties = config.blockAttributes;
2391
+ }
2347
2392
 
2348
- if ( newTreeTop ) {
2349
- newTreeBottom.appendChild( empty( span ) );
2350
- parent.replaceChild( newTreeTop, span );
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;
2351
2399
  }
2400
+ replaceWith( nodeAfterSplit, block );
2401
+ block.appendChild( empty( nodeAfterSplit ) );
2402
+ nodeAfterSplit = block;
2403
+ }
2404
+ return nodeAfterSplit;
2405
+ };
2352
2406
 
2353
- return newTreeBottom || span;
2354
- },
2355
- STRONG: replaceWithTag( 'B' ),
2356
- EM: replaceWithTag( 'I' ),
2357
- STRIKE: replaceWithTag( 'S' ),
2358
- FONT: function ( node, parent ) {
2359
- var face = node.face,
2360
- size = node.size,
2361
- colour = node.color,
2362
- doc = node.ownerDocument,
2363
- fontSpan, sizeSpan, colourSpan,
2364
- newTreeBottom, newTreeTop;
2365
- if ( face ) {
2366
- fontSpan = createElement( doc, 'SPAN', {
2367
- 'class': 'font',
2368
- style: 'font-family:' + face
2369
- });
2370
- newTreeTop = fontSpan;
2371
- newTreeBottom = fontSpan;
2372
- }
2373
- if ( size ) {
2374
- sizeSpan = createElement( doc, 'SPAN', {
2375
- 'class': 'size',
2376
- style: 'font-size:' + fontSizes[ size ] + 'px'
2377
- });
2378
- if ( !newTreeTop ) {
2379
- newTreeTop = sizeSpan;
2380
- }
2381
- if ( newTreeBottom ) {
2382
- newTreeBottom.appendChild( sizeSpan );
2383
- }
2384
- newTreeBottom = sizeSpan;
2385
- }
2386
- if ( colour && /^#?([\dA-F]{3}){1,2}$/i.test( colour ) ) {
2387
- if ( colour.charAt( 0 ) !== '#' ) {
2388
- colour = '#' + colour;
2389
- }
2390
- colourSpan = createElement( doc, 'SPAN', {
2391
- 'class': 'colour',
2392
- style: 'color:' + colour
2393
- });
2394
- if ( !newTreeTop ) {
2395
- newTreeTop = colourSpan;
2396
- }
2397
- if ( newTreeBottom ) {
2398
- newTreeBottom.appendChild( colourSpan );
2399
- }
2400
- newTreeBottom = colourSpan;
2401
- }
2402
- if ( !newTreeTop ) {
2403
- newTreeTop = newTreeBottom = createElement( doc, 'SPAN' );
2404
- }
2405
- parent.replaceChild( newTreeTop, node );
2406
- newTreeBottom.appendChild( empty( node ) );
2407
- return newTreeBottom;
2408
- },
2409
- TT: function ( node, parent ) {
2410
- var el = createElement( node.ownerDocument, 'SPAN', {
2411
- 'class': 'font',
2412
- style: 'font-family:menlo,consolas,"courier new",monospace'
2413
- });
2414
- parent.replaceChild( el, node );
2415
- el.appendChild( empty( node ) );
2416
- return el;
2407
+ proto.forEachBlock = function ( fn, mutates, range ) {
2408
+ if ( !range && !( range = this.getSelection() ) ) {
2409
+ return this;
2410
+ }
2411
+
2412
+ // Save undo checkpoint
2413
+ if ( mutates ) {
2414
+ this._recordUndoState( range );
2415
+ this._getRangeAndRemoveBookmark( range );
2416
+ }
2417
+
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 ) );
2417
2424
  }
2418
- };
2419
2425
 
2420
- var removeEmptyInlines = function ( root ) {
2421
- var children = root.childNodes,
2422
- l = children.length,
2423
- child;
2424
- while ( l-- ) {
2425
- child = children[l];
2426
- if ( child.nodeType === ELEMENT_NODE && !isLeaf( child ) ) {
2427
- removeEmptyInlines( child );
2428
- if ( isInline( child ) && !child.firstChild ) {
2429
- root.removeChild( child );
2430
- }
2431
- } else if ( child.nodeType === TEXT_NODE && !child.data ) {
2432
- root.removeChild( child );
2426
+ if ( mutates ) {
2427
+ this.setSelection( range );
2428
+
2429
+ // Path may have changed
2430
+ this._updatePath( range, true );
2431
+
2432
+ // We're not still in an undo state
2433
+ if ( !canObserveMutations ) {
2434
+ this._docWasChanged();
2433
2435
  }
2434
2436
  }
2437
+ return this;
2435
2438
  };
2436
2439
 
2437
- /*
2438
- Two purposes:
2440
+ proto.modifyBlocks = function ( modify, range ) {
2441
+ if ( !range && !( range = this.getSelection() ) ) {
2442
+ return this;
2443
+ }
2439
2444
 
2440
- 1. Remove nodes we don't want, such as weird <o:p> tags, comment nodes
2441
- and whitespace nodes.
2442
- 2. Convert inline tags into our preferred format.
2443
- */
2444
- var cleanTree = function ( node, allowStyles ) {
2445
- var children = node.childNodes,
2446
- i, l, child, nodeName, nodeType, rewriter, childLength,
2447
- data, j, ll;
2448
- for ( i = 0, l = children.length; i < l; i += 1 ) {
2449
- child = children[i];
2450
- nodeName = child.nodeName;
2451
- nodeType = child.nodeType;
2452
- rewriter = stylesRewriters[ nodeName ];
2453
- if ( nodeType === ELEMENT_NODE ) {
2454
- childLength = child.childNodes.length;
2455
- if ( rewriter ) {
2456
- child = rewriter( child, node );
2457
- } else if ( !allowedBlock.test( nodeName ) &&
2458
- !isInline( child ) ) {
2459
- i -= 1;
2460
- l += childLength - 1;
2461
- node.replaceChild( empty( child ), child );
2462
- continue;
2463
- } else if ( !allowStyles && child.style.cssText ) {
2464
- child.removeAttribute( 'style' );
2465
- }
2466
- if ( childLength ) {
2467
- cleanTree( child, allowStyles );
2468
- }
2469
- } else {
2470
- if ( nodeType === TEXT_NODE ) {
2471
- data = child.data;
2472
- // Use \S instead of notWS, because we want to remove nodes
2473
- // which are just nbsp, in order to cleanup <div>nbsp<br></div>
2474
- // construct.
2475
- if ( /\S/.test( data ) ) {
2476
- // If the parent node is inline, don't trim this node as
2477
- // it probably isn't at the end of the block.
2478
- if ( isInline( node ) ) {
2479
- continue;
2480
- }
2481
- j = 0;
2482
- ll = data.length;
2483
- if ( !i || !isInline( children[ i - 1 ] ) ) {
2484
- while ( j < ll && !notWS.test( data.charAt( j ) ) ) {
2485
- j += 1;
2486
- }
2487
- if ( j ) {
2488
- child.data = data = data.slice( j );
2489
- ll -= j;
2490
- }
2491
- }
2492
- if ( i + 1 === l || !isInline( children[ i + 1 ] ) ) {
2493
- j = ll;
2494
- while ( j > 0 && !notWS.test( data.charAt( j - 1 ) ) ) {
2495
- j -= 1;
2496
- }
2497
- if ( j < ll ) {
2498
- child.data = data.slice( 0, j );
2499
- }
2500
- }
2501
- continue;
2502
- }
2503
- // If we have just white space, it may still be important if it
2504
- // separates two inline nodes, e.g. "<a>link</a> <a>link</a>".
2505
- else if ( i && i + 1 < l &&
2506
- isInline( children[ i - 1 ] ) &&
2507
- isInline( children[ i + 1 ] ) ) {
2508
- child.data = ' ';
2509
- continue;
2510
- }
2511
- }
2512
- node.removeChild( child );
2513
- i -= 1;
2514
- l -= 1;
2515
- }
2445
+ // 1. Save undo checkpoint and bookmark selection
2446
+ if ( this._isInUndoState ) {
2447
+ this._saveRangeToBookmark( range );
2448
+ } else {
2449
+ this._recordUndoState( range );
2516
2450
  }
2517
- return node;
2451
+
2452
+ // 2. Expand range to block boundaries
2453
+ expandRangeToBlockBoundaries( range );
2454
+
2455
+ // 3. Remove range.
2456
+ var body = this._body,
2457
+ frag;
2458
+ moveRangeBoundariesUpTree( range, body );
2459
+ frag = extractContentsOfRange( range, body );
2460
+
2461
+ // 4. Modify tree of fragment and reinsert.
2462
+ insertNodeInRange( range, modify.call( this, frag ) );
2463
+
2464
+ // 5. Merge containers at edges
2465
+ if ( range.endOffset < range.endContainer.childNodes.length ) {
2466
+ mergeContainers( range.endContainer.childNodes[ range.endOffset ] );
2467
+ }
2468
+ mergeContainers( range.startContainer.childNodes[ range.startOffset ] );
2469
+
2470
+ // 6. Restore selection
2471
+ this._getRangeAndRemoveBookmark( range );
2472
+ this.setSelection( range );
2473
+ this._updatePath( range, true );
2474
+
2475
+ // 7. We're not still in an undo state
2476
+ if ( !canObserveMutations ) {
2477
+ this._docWasChanged();
2478
+ }
2479
+
2480
+ return this;
2518
2481
  };
2519
2482
 
2520
- var notWSTextNode = function ( node ) {
2521
- return node.nodeType === ELEMENT_NODE ?
2522
- node.nodeName === 'BR' :
2523
- notWS.test( node.data );
2483
+ var increaseBlockQuoteLevel = function ( frag ) {
2484
+ return this.createElement( 'BLOCKQUOTE',
2485
+ this._config.tagAttributes.blockquote, [
2486
+ frag
2487
+ ]);
2524
2488
  };
2525
- var isLineBreak = function ( br ) {
2526
- var block = br.parentNode,
2527
- walker;
2528
- while ( isInline( block ) ) {
2529
- block = block.parentNode;
2530
- }
2531
- walker = new TreeWalker(
2532
- block, SHOW_ELEMENT|SHOW_TEXT, notWSTextNode );
2533
- walker.currentNode = br;
2534
- return !!walker.nextNode();
2489
+
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;
2535
2498
  };
2536
2499
 
2537
- // <br> elements are treated specially, and differently depending on the
2538
- // browser, when in rich text editor mode. When adding HTML from external
2539
- // sources, we must remove them, replacing the ones that actually affect
2540
- // line breaks with a split of the block element containing it (and wrapping
2541
- // any not inside a block). Browsers that want <br> elements at the end of
2542
- // each block will then have them added back in a later fixCursor method
2543
- // call.
2544
- var cleanupBRs = function ( root ) {
2545
- var brs = root.querySelectorAll( 'BR' ),
2546
- brBreaksLine = [],
2547
- l = brs.length,
2548
- i, br, block;
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
+ };
2549
2512
 
2550
- // Must calculate whether the <br> breaks a line first, because if we
2551
- // have two <br>s next to each other, after the first one is converted
2552
- // to a block split, the second will be at the end of a block and
2553
- // therefore seem to not be a line break. But in its original context it
2554
- // was, so we should also convert it to a block split.
2555
- for ( i = 0; i < l; i += 1 ) {
2556
- brBreaksLine[i] = isLineBreak( brs[i] );
2557
- }
2558
- while ( l-- ) {
2559
- br = brs[l];
2560
- // Cleanup may have removed it
2561
- block = br.parentNode;
2562
- if ( !block ) { continue; }
2563
- while ( isInline( block ) ) {
2564
- block = block.parentNode;
2565
- }
2566
- // If this is not inside a block, replace it by wrapping
2567
- // inlines in a <div>.
2568
- if ( !isBlock( block ) ) {
2569
- fixContainer( block );
2570
- }
2571
- else {
2572
- // If it doesn't break a line, just remove it; it's not doing
2573
- // anything useful. We'll add it back later if required by the
2574
- // browser. If it breaks a line, split the block or leave it as
2575
- // appropriate.
2576
- if ( brBreaksLine[l] ) {
2577
- // If in a <div>, split, but anywhere else we might change
2578
- // the formatting too much (e.g. <li> -> to two list items!)
2579
- // so just play it safe and leave it.
2580
- if ( block.nodeName !== 'DIV' ) {
2581
- continue;
2582
- }
2583
- split( br.parentNode, br, block.parentNode );
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;
2519
+
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;
2526
+ }
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 );
2532
+ }
2533
+ // Otherwise, replace this block with the <ul>/<ol>
2534
+ else {
2535
+ replaceWith(
2536
+ node,
2537
+ self.createElement( type, listAttrs, [
2538
+ newLi
2539
+ ])
2540
+ );
2541
+ }
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
+ );
2584
2550
  }
2585
- detach( br );
2586
2551
  }
2587
2552
  }
2588
2553
  };
2589
2554
 
2590
- proto._ensureBottomLine = function () {
2591
- var body = this._body,
2592
- last = body.lastElementChild;
2593
- if ( !last || last.nodeName !== this.defaultBlockTag || !isBlock( last ) ) {
2594
- body.appendChild( this.createDefaultBlock() );
2595
- }
2555
+ var makeUnorderedList = function ( frag ) {
2556
+ makeList( this, frag, 'UL' );
2557
+ return frag;
2596
2558
  };
2597
2559
 
2598
- // --- Cut and Paste ---
2560
+ var makeOrderedList = function ( frag ) {
2561
+ makeList( this, frag, 'OL' );
2562
+ return frag;
2563
+ };
2599
2564
 
2600
- proto._onCut = function () {
2601
- // Save undo checkpoint
2602
- var range = this.getSelection();
2603
- var self = this;
2604
- this._recordUndoState( range );
2605
- this._getRangeAndRemoveBookmark( range );
2606
- this.setSelection( range );
2607
- setTimeout( function () {
2608
- try {
2609
- // If all content removed, ensure div at start of body.
2610
- self._ensureBottomLine();
2611
- } catch ( error ) {
2612
- self.didError( error );
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 ) );
2613
2576
  }
2614
- }, 0 );
2577
+ fixContainer( listFrag );
2578
+ replaceWith( list, listFrag );
2579
+ }
2580
+ return frag;
2615
2581
  };
2616
2582
 
2617
- proto._onPaste = function ( event ) {
2618
- if ( this._awaitingPaste ) { return; }
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 );
2607
+ }
2608
+ }
2609
+ return frag;
2610
+ };
2619
2611
 
2620
- // Treat image paste as a drop of an image file.
2621
- var clipboardData = event.clipboardData,
2622
- items = clipboardData && clipboardData.items,
2623
- fireDrop = false,
2624
- hasImage = false,
2625
- l, type;
2626
- if ( items ) {
2627
- l = items.length;
2628
- while ( l-- ) {
2629
- type = items[l].type;
2630
- if ( type === 'text/html' ) {
2631
- hasImage = false;
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 );
2624
+ }
2625
+ while ( node ) {
2626
+ next = node.nextSibling;
2627
+ if ( isContainer( node ) ) {
2632
2628
  break;
2633
2629
  }
2634
- if ( /^image\/.*/.test( type ) ) {
2635
- hasImage = true;
2636
- }
2630
+ newParent.insertBefore( node, parent );
2631
+ node = next;
2637
2632
  }
2638
- if ( hasImage ) {
2639
- event.preventDefault();
2640
- this.fireEvent( 'dragover', {
2641
- dataTransfer: clipboardData,
2642
- /*jshint loopfunc: true */
2643
- preventDefault: function () {
2644
- fireDrop = true;
2645
- }
2646
- /*jshint loopfunc: false */
2647
- });
2648
- if ( fireDrop ) {
2649
- this.fireEvent( 'drop', {
2650
- dataTransfer: clipboardData
2651
- });
2652
- }
2653
- return;
2633
+ if ( newParent.nodeName === 'LI' && first.previousSibling ) {
2634
+ split( newParent, first, newParent.parentNode );
2654
2635
  }
2655
- }
2656
-
2657
- this._awaitingPaste = true;
2658
-
2659
- var self = this,
2660
- body = this._body,
2661
- range = this.getSelection(),
2662
- startContainer, startOffset, endContainer, endOffset, startBlock;
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
+ };
2663
2645
 
2664
- // Record undo checkpoint
2665
- self._recordUndoState( range );
2666
- self._getRangeAndRemoveBookmark( range );
2646
+ // --- Clean ---
2667
2647
 
2668
- // Note current selection. We must do this AFTER recording the undo
2669
- // checkpoint, as this modifies the DOM.
2670
- startContainer = range.startContainer;
2671
- startOffset = range.startOffset;
2672
- endContainer = range.endContainer;
2673
- endOffset = range.endOffset;
2674
- startBlock = getStartBlockOfRange( range );
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;
2675
2649
 
2676
- // We need to position the pasteArea in the visible portion of the screen
2677
- // to stop the browser auto-scrolling.
2678
- var pasteArea = this.createElement( 'DIV', {
2679
- style: 'position: absolute; overflow: hidden; top:' +
2680
- ( body.scrollTop +
2681
- ( startBlock ? startBlock.getBoundingClientRect().top : 0 ) ) +
2682
- 'px; left: 0; width: 1px; height: 1px;'
2683
- });
2684
- body.appendChild( pasteArea );
2685
- range.selectNodeContents( pasteArea );
2686
- this.setSelection( range );
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 );
2666
+ }
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
+ }
2677
+ }
2678
+ };
2687
2679
 
2688
- // A setTimeout of 0 means this is added to the back of the
2689
- // single javascript thread, so it will be executed after the
2690
- // paste event.
2691
- setTimeout( function () {
2692
- try {
2693
- // Get the pasted content and clean
2694
- var frag = empty( detach( pasteArea ) ),
2695
- first = frag.firstChild,
2696
- range = self._createRange(
2697
- startContainer, startOffset, endContainer, endOffset );
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)$/;
2698
2681
 
2699
- // Was anything actually pasted?
2700
- if ( first ) {
2701
- // Safari and IE like putting extra divs around things.
2702
- if ( first === frag.lastChild &&
2703
- first.nodeName === 'DIV' ) {
2704
- frag.replaceChild( empty( first ), first );
2705
- }
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
+ };
2706
2691
 
2707
- frag.normalize();
2708
- addLinks( frag );
2709
- cleanTree( frag, false );
2710
- cleanupBRs( frag );
2711
- removeEmptyInlines( frag );
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
+ });
2739
+ }
2740
+ }
2741
+ };
2712
2742
 
2713
- var node = frag,
2714
- doPaste = true;
2715
- while ( node = getNextBlock( node ) ) {
2716
- fixCursor( node );
2717
- }
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
+ };
2718
2751
 
2719
- self.fireEvent( 'willPaste', {
2720
- fragment: frag,
2721
- preventDefault: function () {
2722
- doPaste = false;
2723
- }
2724
- });
2752
+ var stylesRewriters = {
2753
+ SPAN: function ( span, parent ) {
2754
+ var style = span.style,
2755
+ doc = span.ownerDocument,
2756
+ attr, converter, css, newTreeBottom, newTreeTop, el;
2725
2757
 
2726
- // Insert pasted data
2727
- if ( doPaste ) {
2728
- insertTreeFragmentIntoRange( range, frag );
2729
- if ( !canObserveMutations ) {
2730
- self._docWasChanged();
2731
- }
2732
- range.collapse( false );
2733
- self._ensureBottomLine();
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;
2734
2769
  }
2735
2770
  }
2771
+ }
2736
2772
 
2737
- self.setSelection( range );
2738
- self._updatePath( range, true );
2773
+ if ( newTreeTop ) {
2774
+ newTreeBottom.appendChild( empty( span ) );
2775
+ parent.replaceChild( newTreeTop, span );
2776
+ }
2739
2777
 
2740
- self._awaitingPaste = false;
2741
- } catch ( error ) {
2742
- self.didError( error );
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;
2743
2797
  }
2744
- }, 0 );
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 );
2808
+ }
2809
+ newTreeBottom = sizeSpan;
2810
+ }
2811
+ if ( colour && /^#?([\dA-F]{3}){1,2}$/i.test( colour ) ) {
2812
+ if ( colour.charAt( 0 ) !== '#' ) {
2813
+ colour = '#' + colour;
2814
+ }
2815
+ colourSpan = createElement( doc, 'SPAN', {
2816
+ 'class': 'colour',
2817
+ style: 'color:' + colour
2818
+ });
2819
+ if ( !newTreeTop ) {
2820
+ newTreeTop = colourSpan;
2821
+ }
2822
+ if ( newTreeBottom ) {
2823
+ newTreeBottom.appendChild( colourSpan );
2824
+ }
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'
2838
+ });
2839
+ parent.replaceChild( el, node );
2840
+ el.appendChild( empty( node ) );
2841
+ return el;
2842
+ }
2745
2843
  };
2746
2844
 
2747
- // --- Keyboard interaction ---
2748
-
2749
- var keys = {
2750
- 8: 'backspace',
2751
- 9: 'tab',
2752
- 13: 'enter',
2753
- 32: 'space',
2754
- 37: 'left',
2755
- 39: 'right',
2756
- 46: 'delete',
2757
- 219: '[',
2758
- 221: ']'
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
+ }
2859
+ }
2759
2860
  };
2760
2861
 
2761
- var mapKeyTo = function ( method ) {
2762
- return function ( self, event ) {
2763
- event.preventDefault();
2764
- self[ method ]();
2765
- };
2766
- };
2862
+ /*
2863
+ Two purposes:
2767
2864
 
2768
- var mapKeyToFormat = function ( tag, remove ) {
2769
- remove = remove || null;
2770
- return function ( self, event ) {
2771
- event.preventDefault();
2772
- var range = self.getSelection();
2773
- if ( self.hasFormat( tag, null, range ) ) {
2774
- self.changeFormat( null, { tag: tag }, range );
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
+ }
2775
2894
  } else {
2776
- self.changeFormat( { tag: tag }, remove, range );
2777
- }
2778
- };
2779
- };
2780
-
2781
- // If you delete the content inside a span with a font styling, Webkit will
2782
- // replace it with a <font> tag (!). If you delete all the text inside a
2783
- // link in Opera, it won't delete the link. Let's make things consistent. If
2784
- // you delete all text inside an inline tag, remove the inline tag.
2785
- var afterDelete = function ( self, range ) {
2786
- try {
2787
- if ( !range ) { range = self.getSelection(); }
2788
- var node = range.startContainer,
2789
- parent;
2790
- // Climb the tree from the focus point while we are inside an empty
2791
- // inline element
2792
- if ( node.nodeType === TEXT_NODE ) {
2793
- node = node.parentNode;
2794
- }
2795
- parent = node;
2796
- while ( isInline( parent ) &&
2797
- ( !parent.textContent || parent.textContent === ZWS ) ) {
2798
- node = parent;
2799
- parent = node.parentNode;
2800
- }
2801
- // If focussed in empty inline element
2802
- if ( node !== parent ) {
2803
- // Move focus to just before empty inline(s)
2804
- range.setStart( parent,
2805
- indexOf.call( parent.childNodes, node ) );
2806
- range.collapse( true );
2807
- // Remove empty inline(s)
2808
- parent.removeChild( node );
2809
- // Fix cursor in block
2810
- if ( !isBlock( parent ) ) {
2811
- parent = getPreviousBlock( parent );
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
+ }
2812
2936
  }
2813
- fixCursor( parent );
2814
- // Move cursor into text node
2815
- moveRangeBoundariesDownTree( range );
2937
+ node.removeChild( child );
2938
+ i -= 1;
2939
+ l -= 1;
2816
2940
  }
2817
- self._ensureBottomLine();
2818
- self.setSelection( range );
2819
- self._updatePath( range, true );
2820
- } catch ( error ) {
2821
- self.didError( error );
2822
2941
  }
2942
+ return node;
2823
2943
  };
2824
2944
 
2825
- var keyHandlers = {
2826
- enter: function ( self, event, range ) {
2827
- var block, parent, nodeAfterSplit;
2828
-
2829
- // We handle this ourselves
2830
- event.preventDefault();
2831
-
2832
- // Save undo checkpoint and add any links in the preceding section.
2833
- // Remove any zws so we don't think there's content in an empty
2834
- // block.
2835
- self._recordUndoState( range );
2836
- addLinks( range.startContainer );
2837
- self._removeZWS();
2838
- self._getRangeAndRemoveBookmark( range );
2839
-
2840
- // Selected text is overwritten, therefore delete the contents
2841
- // to collapse selection.
2842
- if ( !range.collapsed ) {
2843
- deleteContentsOfRange( range );
2844
- }
2945
+ var notWSTextNode = function ( node ) {
2946
+ return node.nodeType === ELEMENT_NODE ?
2947
+ node.nodeName === 'BR' :
2948
+ notWS.test( node.data );
2949
+ };
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();
2960
+ };
2845
2961
 
2846
- block = getStartBlockOfRange( range );
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;
2847
2974
 
2848
- // If this is a malformed bit of document or in a table;
2849
- // just play it safe and insert a <br>.
2850
- if ( !block || /^T[HD]$/.test( block.nodeName ) ) {
2851
- insertNodeInRange( range, self.createElement( 'BR' ) );
2852
- range.collapse( false );
2853
- self.setSelection( range );
2854
- self._updatePath( range, true );
2855
- return;
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] );
2982
+ }
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;
2856
2990
  }
2857
-
2858
- // If in a list, we'll split the LI instead.
2859
- if ( parent = getNearest( block, 'LI' ) ) {
2860
- block = parent;
2991
+ // If this is not inside a block, replace it by wrapping
2992
+ // inlines in a <div>.
2993
+ if ( !isBlock( block ) ) {
2994
+ fixContainer( block );
2861
2995
  }
2862
-
2863
- if ( !block.textContent ) {
2864
- // Break list
2865
- if ( getNearest( block, 'UL' ) || getNearest( block, 'OL' ) ) {
2866
- return self.modifyBlocks( decreaseListLevel, range );
2867
- }
2868
- // Break blockquote
2869
- else if ( getNearest( block, 'BLOCKQUOTE' ) ) {
2870
- return self.modifyBlocks( removeBlockQuote, range );
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 );
2871
3009
  }
3010
+ detach( br );
2872
3011
  }
3012
+ }
3013
+ };
2873
3014
 
2874
- // Otherwise, split at cursor point.
2875
- nodeAfterSplit = splitBlock( self, block,
2876
- range.startContainer, range.startOffset );
2877
-
2878
- // Clean up any empty inlines if we hit enter at the beginning of the
2879
- // block
2880
- removeZWS( block );
2881
- removeEmptyInlines( block );
2882
- fixCursor( block );
2883
-
2884
- // Focus cursor
2885
- // If there's a <b>/<i> etc. at the beginning of the split
2886
- // make sure we focus inside it.
2887
- while ( nodeAfterSplit.nodeType === ELEMENT_NODE ) {
2888
- var child = nodeAfterSplit.firstChild,
2889
- next;
2890
-
2891
- // Don't continue links over a block break; unlikely to be the
2892
- // desired outcome.
2893
- if ( nodeAfterSplit.nodeName === 'A' &&
2894
- ( !nodeAfterSplit.textContent ||
2895
- nodeAfterSplit.textContent === ZWS ) ) {
2896
- child = self._doc.createTextNode( '' );
2897
- replaceWith( nodeAfterSplit, child );
2898
- nodeAfterSplit = child;
2899
- break;
2900
- }
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() );
3021
+ }
3022
+ };
2901
3023
 
2902
- while ( child && child.nodeType === TEXT_NODE && !child.data ) {
2903
- next = child.nextSibling;
2904
- if ( !next || next.nodeName === 'BR' ) {
2905
- break;
2906
- }
2907
- detach( child );
2908
- child = next;
2909
- }
3024
+ // --- Cut and Paste ---
2910
3025
 
2911
- // 'BR's essentially don't count; they're a browser hack.
2912
- // If you try to select the contents of a 'BR', FF will not let
2913
- // you type anything!
2914
- if ( !child || child.nodeName === 'BR' ||
2915
- ( child.nodeType === TEXT_NODE && !isPresto ) ) {
2916
- break;
2917
- }
2918
- nodeAfterSplit = child;
3026
+ proto._onCut = function () {
3027
+ // 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 );
2919
3039
  }
2920
- range = self._createRange( nodeAfterSplit, 0 );
2921
- self.setSelection( range );
2922
- self._updatePath( range, true );
3040
+ }, 0 );
3041
+ };
2923
3042
 
2924
- // Scroll into view
2925
- if ( nodeAfterSplit.nodeType === TEXT_NODE ) {
2926
- nodeAfterSplit = nodeAfterSplit.parentNode;
2927
- }
2928
- var doc = self._doc,
2929
- body = self._body;
2930
- if ( nodeAfterSplit.offsetTop + nodeAfterSplit.offsetHeight >
2931
- ( doc.documentElement.scrollTop || body.scrollTop ) +
2932
- body.offsetHeight ) {
2933
- nodeAfterSplit.scrollIntoView( false );
2934
- }
2935
- },
2936
- backspace: function ( self, event, range ) {
2937
- self._removeZWS();
2938
- // Record undo checkpoint.
2939
- self._recordUndoState( range );
2940
- self._getRangeAndRemoveBookmark( range );
2941
- // If not collapsed, delete contents
2942
- if ( !range.collapsed ) {
2943
- event.preventDefault();
2944
- deleteContentsOfRange( range );
2945
- afterDelete( self, range );
2946
- }
2947
- // If at beginning of block, merge with previous
2948
- else if ( rangeDoesStartAtBlockBoundary( range ) ) {
2949
- event.preventDefault();
2950
- var current = getStartBlockOfRange( range ),
2951
- previous = current && getPreviousBlock( current );
2952
- // Must not be at the very beginning of the text area.
2953
- if ( previous ) {
2954
- // If not editable, just delete whole block.
2955
- if ( !previous.isContentEditable ) {
2956
- detach( previous );
2957
- return;
2958
- }
2959
- // Otherwise merge.
2960
- mergeWithBlock( previous, current, range );
2961
- // If deleted line between containers, merge newly adjacent
2962
- // containers.
2963
- current = previous.parentNode;
2964
- while ( current && !current.nextSibling ) {
2965
- current = current.parentNode;
2966
- }
2967
- if ( current && ( current = current.nextSibling ) ) {
2968
- mergeContainers( current );
2969
- }
2970
- self.setSelection( range );
3043
+ proto._onPaste = function ( event ) {
3044
+ if ( this._awaitingPaste ) { return; }
3045
+
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;
2971
3059
  }
2972
- // If at very beginning of text area, allow backspace
2973
- // to break lists/blockquote.
2974
- else if ( current ) {
2975
- // Break list
2976
- if ( getNearest( current, 'UL' ) ||
2977
- getNearest( current, 'OL' ) ) {
2978
- return self.modifyBlocks( decreaseListLevel, range );
2979
- }
2980
- // Break blockquote
2981
- else if ( getNearest( current, 'BLOCKQUOTE' ) ) {
2982
- return self.modifyBlocks( decreaseBlockQuoteLevel, range );
2983
- }
2984
- self.setSelection( range );
2985
- self._updatePath( range, true );
3060
+ if ( /^image\/.*/.test( type ) ) {
3061
+ hasImage = true;
2986
3062
  }
2987
3063
  }
2988
- // Otherwise, leave to browser but check afterwards whether it has
2989
- // left behind an empty inline tag.
2990
- else {
2991
- self.setSelection( range );
2992
- setTimeout( function () { afterDelete( self ); }, 0 );
2993
- }
2994
- },
2995
- 'delete': function ( self, event, range ) {
2996
- self._removeZWS();
2997
- // Record undo checkpoint.
2998
- self._recordUndoState( range );
2999
- self._getRangeAndRemoveBookmark( range );
3000
- // If not collapsed, delete contents
3001
- if ( !range.collapsed ) {
3002
- event.preventDefault();
3003
- deleteContentsOfRange( range );
3004
- afterDelete( self, range );
3005
- }
3006
- // If at end of block, merge next into this block
3007
- else if ( rangeDoesEndAtBlockBoundary( range ) ) {
3064
+ if ( hasImage ) {
3008
3065
  event.preventDefault();
3009
- var current = getStartBlockOfRange( range ),
3010
- next = current && getNextBlock( current );
3011
- // Must not be at the very end of the text area.
3012
- if ( next ) {
3013
- // If not editable, just delete whole block.
3014
- if ( !next.isContentEditable ) {
3015
- detach( next );
3016
- return;
3017
- }
3018
- // Otherwise merge.
3019
- mergeWithBlock( current, next, range );
3020
- // If deleted line between containers, merge newly adjacent
3021
- // containers.
3022
- next = current.parentNode;
3023
- while ( next && !next.nextSibling ) {
3024
- next = next.parentNode;
3025
- }
3026
- if ( next && ( next = next.nextSibling ) ) {
3027
- mergeContainers( next );
3066
+ this.fireEvent( 'dragover', {
3067
+ dataTransfer: clipboardData,
3068
+ /*jshint loopfunc: true */
3069
+ preventDefault: function () {
3070
+ fireDrop = true;
3028
3071
  }
3029
- self.setSelection( range );
3030
- self._updatePath( range, true );
3072
+ /*jshint loopfunc: false */
3073
+ });
3074
+ if ( fireDrop ) {
3075
+ this.fireEvent( 'drop', {
3076
+ dataTransfer: clipboardData
3077
+ });
3031
3078
  }
3079
+ return;
3032
3080
  }
3033
- // Otherwise, leave to browser but check afterwards whether it has
3034
- // left behind an empty inline tag.
3035
- else {
3036
- self.setSelection( range );
3037
- setTimeout( function () { afterDelete( self ); }, 0 );
3038
- }
3039
- },
3040
- tab: function ( self, event, range ) {
3041
- var node, parent;
3042
- self._removeZWS();
3043
- // If no selection and in an empty block
3044
- if ( range.collapsed &&
3045
- rangeDoesStartAtBlockBoundary( range ) &&
3046
- rangeDoesEndAtBlockBoundary( range ) ) {
3047
- node = getStartBlockOfRange( range );
3048
- // Iterate through the block's parents
3049
- while ( parent = node.parentNode ) {
3050
- // If we find a UL or OL (so are in a list, node must be an LI)
3051
- if ( parent.nodeName === 'UL' || parent.nodeName === 'OL' ) {
3052
- // AND the LI is not the first in the list
3053
- if ( node.previousSibling ) {
3054
- // Then increase the list level
3055
- event.preventDefault();
3056
- self.modifyBlocks( increaseListLevel, range );
3081
+ }
3082
+
3083
+ this._awaitingPaste = true;
3084
+
3085
+ var self = this,
3086
+ body = this._body,
3087
+ range = this.getSelection(),
3088
+ startContainer, startOffset, endContainer, endOffset, startBlock;
3089
+
3090
+ // Record undo checkpoint
3091
+ self._recordUndoState( range );
3092
+ self._getRangeAndRemoveBookmark( range );
3093
+
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 );
3101
+
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 );
3112
+ this.setSelection( range );
3113
+
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 );
3124
+
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
+ }
3132
+
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
+ }
3153
+
3154
+ self.fireEvent( 'willPaste', event );
3155
+
3156
+ // Insert pasted data
3157
+ if ( doPaste ) {
3158
+ insertTreeFragmentIntoRange( range, event.fragment );
3159
+ if ( !canObserveMutations ) {
3160
+ self._docWasChanged();
3057
3161
  }
3058
- break;
3162
+ range.collapse( false );
3163
+ self._ensureBottomLine();
3059
3164
  }
3060
- node = parent;
3061
3165
  }
3062
- event.preventDefault();
3063
- }
3064
- },
3065
- space: function ( self, _, range ) {
3066
- var node, parent;
3067
- self._recordUndoState( range );
3068
- addLinks( range.startContainer );
3069
- self._getRangeAndRemoveBookmark( range );
3070
3166
 
3071
- // If the cursor is at the end of a link (<a>foo|</a>) then move it
3072
- // outside of the link (<a>foo</a>|) so that the space is not part of
3073
- // the link text.
3074
- node = range.endContainer;
3075
- parent = node.parentNode;
3076
- if ( range.collapsed && parent.nodeName === 'A' &&
3077
- !node.nextSibling && range.endOffset === getLength( node ) ) {
3078
- range.setStartAfter( parent );
3079
- }
3167
+ self.setSelection( range );
3168
+ self._updatePath( range, true );
3080
3169
 
3081
- self.setSelection( range );
3082
- },
3083
- left: function ( self ) {
3084
- self._removeZWS();
3085
- },
3086
- right: function ( self ) {
3087
- self._removeZWS();
3088
- }
3170
+ self._awaitingPaste = false;
3171
+ } catch ( error ) {
3172
+ self.didError( error );
3173
+ }
3174
+ }, 0 );
3089
3175
  };
3090
3176
 
3091
- // Firefox incorrectly handles Cmd-left/Cmd-right on Mac:
3092
- // it goes back/forward in history! Override to do the right
3093
- // thing.
3094
- // https://bugzilla.mozilla.org/show_bug.cgi?id=289384
3095
- if ( isMac && isGecko && win.getSelection().modify ) {
3096
- keyHandlers[ 'meta-left' ] = function ( self, event ) {
3097
- event.preventDefault();
3098
- self._sel.modify( 'move', 'backward', 'lineboundary' );
3099
- };
3100
- keyHandlers[ 'meta-right' ] = function ( self, event ) {
3101
- event.preventDefault();
3102
- self._sel.modify( 'move', 'forward', 'lineboundary' );
3103
- };
3104
- }
3177
+ // --- Keyboard interaction ---
3105
3178
 
3106
- keyHandlers[ ctrlKey + 'b' ] = mapKeyToFormat( 'B' );
3107
- keyHandlers[ ctrlKey + 'i' ] = mapKeyToFormat( 'I' );
3108
- keyHandlers[ ctrlKey + 'u' ] = mapKeyToFormat( 'U' );
3109
- keyHandlers[ ctrlKey + 'shift-7' ] = mapKeyToFormat( 'S' );
3110
- keyHandlers[ ctrlKey + 'shift-5' ] = mapKeyToFormat( 'SUB', { tag: 'SUP' } );
3111
- keyHandlers[ ctrlKey + 'shift-6' ] = mapKeyToFormat( 'SUP', { tag: 'SUB' } );
3112
- keyHandlers[ ctrlKey + 'shift-8' ] = mapKeyTo( 'makeUnorderedList' );
3113
- keyHandlers[ ctrlKey + 'shift-9' ] = mapKeyTo( 'makeOrderedList' );
3114
- keyHandlers[ ctrlKey + '[' ] = mapKeyTo( 'decreaseQuoteLevel' );
3115
- keyHandlers[ ctrlKey + ']' ] = mapKeyTo( 'increaseQuoteLevel' );
3116
- keyHandlers[ ctrlKey + 'y' ] = mapKeyTo( 'redo' );
3117
- keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( 'undo' );
3118
- keyHandlers[ ctrlKey + 'shift-z' ] = mapKeyTo( 'redo' );
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: ']'
3189
+ };
3119
3190
 
3120
3191
  // Ref: http://unixpapa.com/js/key.html
3121
3192
  proto._onKey = function ( event ) {
@@ -3156,8 +3227,8 @@ proto._onKey = function ( event ) {
3156
3227
 
3157
3228
  key = modifiers + key;
3158
3229
 
3159
- if ( keyHandlers[ key ] ) {
3160
- keyHandlers[ key ]( this, event, range );
3230
+ if ( this._keyHandlers[ key ] ) {
3231
+ this._keyHandlers[ key ]( this, event, range );
3161
3232
  } else if ( key.length === 1 && !range.collapsed ) {
3162
3233
  // Record undo checkpoint.
3163
3234
  this._recordUndoState( range );
@@ -3170,6 +3241,11 @@ proto._onKey = function ( event ) {
3170
3241
  }
3171
3242
  };
3172
3243
 
3244
+ proto.setKeyHandler = function ( key, fn ) {
3245
+ this._keyHandlers[ key ] = fn;
3246
+ return this;
3247
+ };
3248
+
3173
3249
  // --- Get/Set data ---
3174
3250
 
3175
3251
  proto._getHTML = function () {
@@ -3311,10 +3387,10 @@ proto.insertElement = function ( el, range ) {
3311
3387
  return this;
3312
3388
  };
3313
3389
 
3314
- proto.insertImage = function ( src ) {
3315
- var img = this.createElement( 'IMG', {
3390
+ proto.insertImage = function ( src, attributes ) {
3391
+ var img = this.createElement( 'IMG', mergeObjects({
3316
3392
  src: src
3317
- });
3393
+ }, attributes ));
3318
3394
  this.insertElement( img );
3319
3395
  return img;
3320
3396
  };
@@ -3511,13 +3587,6 @@ proto.setTextAlignment = function ( alignment ) {
3511
3587
 
3512
3588
  proto.setTextDirection = function ( direction ) {
3513
3589
  this.forEachBlock( function ( block ) {
3514
- block.className = ( block.className
3515
- .split( /\s+/ )
3516
- .filter( function ( klass ) {
3517
- return !( /dir/.test( klass ) );
3518
- })
3519
- .join( ' ' ) +
3520
- ' dir-' + direction ).trim();
3521
3590
  block.dir = direction;
3522
3591
  }, true );
3523
3592
  return this.focus();
@@ -3540,7 +3609,11 @@ if ( top !== win ) {
3540
3609
  win.onEditorLoad = null;
3541
3610
  }
3542
3611
  } else {
3543
- win.Squire = Squire;
3612
+ if ( typeof exports === 'object' ) {
3613
+ module.exports = Squire;
3614
+ } else {
3615
+ win.Squire = Squire;
3616
+ }
3544
3617
  }
3545
3618
 
3546
3619
  }( document ) );