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.
- checksums.yaml +4 -4
- data/lib/squire-rails/version.rb +1 -1
- data/vendor/assets/javascripts/squire/squire-raw.js +1867 -1794
- metadata +2 -2
@@ -1,4 +1,4 @@
|
|
1
|
-
/* Copyright © 2011-
|
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
|
306
|
+
fixer, child;
|
295
307
|
|
296
308
|
if ( node.nodeName === 'BODY' ) {
|
297
309
|
if ( !( child = node.firstChild ) || child.nodeName === 'BR' ) {
|
298
|
-
|
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 ) {
|
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 ) {
|
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
|
1076
|
+
var mapKeyTo = function ( method ) {
|
1077
|
+
return function ( self, event ) {
|
1078
|
+
event.preventDefault();
|
1079
|
+
self[ method ]();
|
1080
|
+
};
|
1081
|
+
};
|
1060
1082
|
|
1061
|
-
function
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1066
|
-
if (
|
1067
|
-
|
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
|
-
|
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
|
-
|
1088
|
-
|
1089
|
-
|
1090
|
-
|
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
|
-
|
1094
|
-
|
1095
|
-
|
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
|
-
|
1103
|
-
|
1144
|
+
// We handle this ourselves
|
1145
|
+
event.preventDefault();
|
1104
1146
|
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1109
|
-
|
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
|
-
|
1112
|
-
|
1113
|
-
|
1114
|
-
|
1115
|
-
|
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
|
-
|
1125
|
-
this.defaultBlockProperties = null;
|
1161
|
+
block = getStartBlockOfRange( range );
|
1126
1162
|
|
1127
|
-
|
1128
|
-
|
1129
|
-
|
1130
|
-
|
1131
|
-
|
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
|
-
|
1134
|
-
|
1173
|
+
// If in a list, we'll split the LI instead.
|
1174
|
+
if ( parent = getNearest( block, 'LI' ) ) {
|
1175
|
+
block = parent;
|
1176
|
+
}
|
1135
1177
|
|
1136
|
-
|
1137
|
-
|
1138
|
-
|
1139
|
-
|
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
|
-
|
1159
|
-
|
1183
|
+
// Break blockquote
|
1184
|
+
else if ( getNearest( block, 'BLOCKQUOTE' ) ) {
|
1185
|
+
return self.modifyBlocks( removeBlockQuote, range );
|
1160
1186
|
}
|
1161
|
-
|
1162
|
-
};
|
1163
|
-
}
|
1187
|
+
}
|
1164
1188
|
|
1165
|
-
|
1166
|
-
|
1189
|
+
// Otherwise, split at cursor point.
|
1190
|
+
nodeAfterSplit = splitBlock( self, block,
|
1191
|
+
range.startContainer, range.startOffset );
|
1167
1192
|
|
1168
|
-
|
1169
|
-
|
1170
|
-
|
1171
|
-
|
1172
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
1180
|
-
|
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
|
-
|
1184
|
-
|
1185
|
-
|
1186
|
-
|
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
|
-
|
1191
|
-
|
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
|
-
|
1216
|
-
|
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
|
-
|
1219
|
-
|
1220
|
-
|
1221
|
-
|
1222
|
-
|
1223
|
-
|
1224
|
-
|
1225
|
-
|
1226
|
-
|
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
|
-
|
1229
|
-
|
1230
|
-
|
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
|
-
|
1235
|
-
|
1236
|
-
|
1237
|
-
|
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
|
-
|
1261
|
-
|
1262
|
-
|
1263
|
-
|
1264
|
-
|
1265
|
-
|
1266
|
-
|
1267
|
-
|
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
|
-
|
1280
|
-
|
1281
|
-
|
1282
|
-
|
1283
|
-
|
1284
|
-
|
1285
|
-
|
1286
|
-
|
1287
|
-
|
1288
|
-
|
1289
|
-
|
1290
|
-
|
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
|
-
|
1294
|
-
|
1295
|
-
|
1296
|
-
|
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
|
-
|
1301
|
-
|
1302
|
-
|
1303
|
-
|
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
|
-
|
1321
|
-
|
1322
|
-
//
|
1323
|
-
|
1324
|
-
|
1325
|
-
|
1326
|
-
|
1327
|
-
|
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
|
-
|
1337
|
-
|
1338
|
-
|
1339
|
-
|
1340
|
-
|
1341
|
-
|
1342
|
-
|
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
|
-
|
1361
|
-
|
1362
|
-
|
1363
|
-
|
1364
|
-
|
1365
|
-
|
1366
|
-
|
1367
|
-
|
1368
|
-
|
1369
|
-
|
1370
|
-
|
1371
|
-
|
1372
|
-
|
1373
|
-
|
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
|
-
|
1377
|
-
|
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
|
-
|
1381
|
-
|
1382
|
-
|
1383
|
-
|
1384
|
-
|
1385
|
-
|
1386
|
-
|
1387
|
-
|
1388
|
-
|
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
|
-
|
1402
|
-
|
1403
|
-
|
1404
|
-
|
1405
|
-
|
1406
|
-
|
1407
|
-
|
1408
|
-
|
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
|
-
|
1463
|
+
function Squire ( doc, config ) {
|
1464
|
+
var win = doc.defaultView;
|
1465
|
+
var body = doc.body;
|
1466
|
+
var mutation;
|
1411
1467
|
|
1412
|
-
|
1413
|
-
|
1414
|
-
|
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
|
-
|
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
|
-
|
1474
|
+
this._sel = win.getSelection();
|
1475
|
+
this._lastSelection = null;
|
1445
1476
|
|
1446
|
-
|
1447
|
-
|
1448
|
-
|
1449
|
-
|
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
|
-
|
1467
|
-
this._updatePath( this.getSelection() );
|
1468
|
-
};
|
1483
|
+
this._hasZWS = false;
|
1469
1484
|
|
1470
|
-
|
1485
|
+
this._lastAnchorNode = null;
|
1486
|
+
this._lastFocusNode = null;
|
1487
|
+
this._path = '';
|
1471
1488
|
|
1472
|
-
|
1473
|
-
|
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
|
-
|
1485
|
-
|
1486
|
-
|
1487
|
-
|
1488
|
-
|
1489
|
-
|
1490
|
-
|
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
|
-
//
|
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
|
-
|
1499
|
-
|
1520
|
+
// Opera does not fire keydown repeatedly.
|
1521
|
+
this.addEventListener( isPresto ? 'keypress' : 'keydown', this._onKey );
|
1500
1522
|
|
1501
|
-
|
1502
|
-
|
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
|
-
|
1513
|
-
|
1514
|
-
insertNodeInRange( range, endNode );
|
1526
|
+
// Override default properties
|
1527
|
+
this.setConfig( config );
|
1515
1528
|
|
1516
|
-
//
|
1517
|
-
|
1518
|
-
|
1519
|
-
|
1520
|
-
|
1521
|
-
|
1522
|
-
|
1523
|
-
|
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
|
-
|
1527
|
-
range.setEndBefore( endNode );
|
1528
|
-
};
|
1558
|
+
body.setAttribute( 'contenteditable', 'true' );
|
1529
1559
|
|
1530
|
-
|
1531
|
-
|
1532
|
-
|
1533
|
-
|
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
|
-
|
1536
|
-
var startContainer = start.parentNode,
|
1537
|
-
endContainer = end.parentNode,
|
1538
|
-
collapsed;
|
1566
|
+
instances.push( this );
|
1539
1567
|
|
1540
|
-
|
1541
|
-
|
1542
|
-
|
1543
|
-
|
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
|
-
|
1548
|
-
|
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
|
-
|
1552
|
-
|
1587
|
+
// Users may specify block tag in lower case
|
1588
|
+
config.blockTag = config.blockTag.toUpperCase();
|
1553
1589
|
|
1554
|
-
|
1555
|
-
mergeInlines( startContainer, _range );
|
1556
|
-
if ( startContainer !== endContainer ) {
|
1557
|
-
mergeInlines( endContainer, _range );
|
1558
|
-
}
|
1590
|
+
this._config = config;
|
1559
1591
|
|
1560
|
-
|
1561
|
-
|
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
|
-
|
1568
|
-
|
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
|
-
|
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.
|
1578
|
-
|
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.
|
1591
|
-
|
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
|
-
//
|
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
|
-
|
1614
|
-
|
1615
|
-
|
1616
|
+
// Subscribing to these events won't automatically add a listener to the
|
1617
|
+
// document node, since these events are fired in a custom manner by the
|
1618
|
+
// editor code.
|
1619
|
+
var customEvents = {
|
1620
|
+
focus: 1, blur: 1,
|
1621
|
+
pathChange: 1, select: 1, input: 1, undoStateChange: 1
|
1622
|
+
};
|
1623
|
+
|
1624
|
+
proto.fireEvent = function ( type, event ) {
|
1625
|
+
var handlers = this._events[ type ],
|
1626
|
+
i, l, obj;
|
1627
|
+
if ( handlers ) {
|
1628
|
+
if ( !event ) {
|
1629
|
+
event = {};
|
1630
|
+
}
|
1631
|
+
if ( event.type !== type ) {
|
1632
|
+
event.type = type;
|
1633
|
+
}
|
1634
|
+
// Clone handlers array, so any handlers added/removed do not affect it.
|
1635
|
+
handlers = handlers.slice();
|
1636
|
+
for ( i = 0, l = handlers.length; i < l; i += 1 ) {
|
1637
|
+
obj = handlers[i];
|
1638
|
+
try {
|
1639
|
+
if ( obj.handleEvent ) {
|
1640
|
+
obj.handleEvent( event );
|
1641
|
+
} else {
|
1642
|
+
obj.call( this, event );
|
1643
|
+
}
|
1644
|
+
} catch ( error ) {
|
1645
|
+
error.details = 'Squire: fireEvent error. Event type: ' + type;
|
1646
|
+
this.didError( error );
|
1647
|
+
}
|
1616
1648
|
}
|
1649
|
+
}
|
1650
|
+
return this;
|
1651
|
+
};
|
1617
1652
|
|
1618
|
-
|
1619
|
-
|
1620
|
-
|
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.
|
1629
|
-
|
1630
|
-
|
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
|
-
|
1635
|
-
|
1636
|
-
|
1637
|
-
|
1638
|
-
|
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
|
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.
|
1651
|
-
|
1652
|
-
|
1653
|
-
|
1654
|
-
|
1655
|
-
|
1656
|
-
|
1657
|
-
|
1658
|
-
|
1659
|
-
|
1660
|
-
|
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
|
-
// ---
|
1719
|
+
// --- Selection and Path ---
|
1672
1720
|
|
1673
|
-
|
1674
|
-
|
1675
|
-
|
1676
|
-
|
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
|
-
|
1684
|
-
|
1685
|
-
|
1686
|
-
|
1687
|
-
|
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
|
-
|
1692
|
-
|
1693
|
-
|
1694
|
-
|
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
|
-
|
1698
|
-
|
1699
|
-
|
1700
|
-
|
1701
|
-
|
1702
|
-
|
1703
|
-
|
1704
|
-
|
1705
|
-
if (
|
1706
|
-
|
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
|
-
|
1763
|
+
if ( endContainer && isLeaf( endContainer ) ) {
|
1764
|
+
selection.setEndBefore( endContainer );
|
1765
|
+
}
|
1766
|
+
this._lastSelection = selection;
|
1767
|
+
} else {
|
1768
|
+
selection = this._lastSelection;
|
1709
1769
|
}
|
1710
|
-
|
1711
|
-
|
1770
|
+
if ( !selection ) {
|
1771
|
+
selection = this._createRange( this._body.firstChild, 0 );
|
1772
|
+
}
|
1773
|
+
return selection;
|
1712
1774
|
};
|
1713
1775
|
|
1714
|
-
proto.
|
1715
|
-
|
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
|
1743
|
-
|
1744
|
-
|
1745
|
-
|
1746
|
-
|
1747
|
-
|
1748
|
-
|
1749
|
-
|
1750
|
-
|
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
|
-
|
1764
|
-
|
1765
|
-
|
1766
|
-
}
|
1792
|
+
if ( !walker.filter( node ) ) {
|
1793
|
+
node = walker.nextNode();
|
1794
|
+
}
|
1767
1795
|
|
1768
|
-
|
1769
|
-
|
1770
|
-
|
1771
|
-
if (
|
1772
|
-
|
1773
|
-
|
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
|
1778
|
-
|
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
|
-
|
1787
|
-
|
1788
|
-
el.appendChild( node );
|
1806
|
+
textContent += value;
|
1807
|
+
addedTextInBlock = true;
|
1789
1808
|
}
|
1790
|
-
}
|
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
|
-
|
1793
|
-
|
1794
|
-
|
1795
|
-
|
1796
|
-
|
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
|
-
|
1799
|
-
// one child
|
1800
|
-
endContainer = node.parentNode;
|
1801
|
-
endOffset = 1;
|
1843
|
+
node.deleteData( index, 1 );
|
1802
1844
|
}
|
1803
1845
|
}
|
1846
|
+
}
|
1847
|
+
};
|
1804
1848
|
|
1805
|
-
|
1806
|
-
|
1807
|
-
|
1849
|
+
proto._didAddZWS = function () {
|
1850
|
+
this._hasZWS = true;
|
1851
|
+
};
|
1852
|
+
proto._removeZWS = function () {
|
1853
|
+
if ( !this._hasZWS ) {
|
1854
|
+
return;
|
1808
1855
|
}
|
1809
|
-
|
1856
|
+
removeZWS( this._body );
|
1857
|
+
this._hasZWS = false;
|
1810
1858
|
};
|
1811
1859
|
|
1812
|
-
|
1813
|
-
// Add bookmark
|
1814
|
-
this._saveRangeToBookmark( range );
|
1860
|
+
// --- Path change events ---
|
1815
1861
|
|
1816
|
-
|
1817
|
-
|
1818
|
-
|
1819
|
-
|
1820
|
-
if (
|
1821
|
-
|
1822
|
-
|
1823
|
-
|
1824
|
-
|
1825
|
-
|
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
|
-
|
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
|
-
|
1837
|
-
|
1838
|
-
|
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
|
-
|
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
|
-
|
1865
|
-
|
1866
|
-
|
1867
|
-
|
1868
|
-
|
1869
|
-
|
1870
|
-
|
1871
|
-
|
1872
|
-
|
1873
|
-
|
1874
|
-
|
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
|
-
|
1892
|
-
|
1893
|
-
|
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
|
-
|
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
|
-
|
1911
|
-
|
1912
|
-
|
1913
|
-
|
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
|
-
|
1942
|
+
range.setStartAfter( startNode );
|
1943
|
+
range.setEndBefore( endNode );
|
1926
1944
|
};
|
1927
1945
|
|
1928
|
-
proto.
|
1929
|
-
|
1930
|
-
|
1931
|
-
|
1932
|
-
}
|
1946
|
+
proto._getRangeAndRemoveBookmark = function ( range ) {
|
1947
|
+
var doc = this._doc,
|
1948
|
+
start = doc.getElementById( startSelectionId ),
|
1949
|
+
end = doc.getElementById( endSelectionId );
|
1933
1950
|
|
1934
|
-
|
1935
|
-
|
1936
|
-
|
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
|
-
|
1939
|
-
|
1940
|
-
|
1941
|
-
|
1942
|
-
|
1943
|
-
|
1944
|
-
|
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
|
-
|
1948
|
-
this._updatePath( range, true );
|
1991
|
+
// --- Undo ---
|
1949
1992
|
|
1950
|
-
|
1951
|
-
|
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
|
-
|
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
|
-
//
|
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
|
-
|
1961
|
-
|
1962
|
-
|
1963
|
-
|
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
|
-
|
1967
|
-
|
1968
|
-
|
1969
|
-
|
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
|
-
|
1972
|
-
|
1973
|
-
|
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
|
-
|
1977
|
-
|
1978
|
-
|
1979
|
-
|
1980
|
-
|
1981
|
-
|
1982
|
-
|
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
|
-
|
1985
|
-
|
1986
|
-
|
2078
|
+
this.fireEvent( 'undoStateChange', {
|
2079
|
+
canUndo: true,
|
2080
|
+
canRedo: undoIndex + 2 < undoStackLength
|
2081
|
+
});
|
2082
|
+
this.fireEvent( 'input' );
|
1987
2083
|
}
|
1988
|
-
return
|
2084
|
+
return this;
|
1989
2085
|
};
|
1990
2086
|
|
1991
|
-
|
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
|
2096
|
+
return false;
|
1994
2097
|
}
|
1995
2098
|
|
1996
|
-
//
|
1997
|
-
|
1998
|
-
|
1999
|
-
|
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
|
-
|
2003
|
-
|
2004
|
-
if (
|
2005
|
-
|
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
|
-
|
2011
|
-
|
2012
|
-
|
2013
|
-
|
2014
|
-
|
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
|
-
|
2017
|
-
|
2018
|
-
|
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
|
-
|
2126
|
+
|
2127
|
+
return seenNode;
|
2022
2128
|
};
|
2023
2129
|
|
2024
|
-
proto.
|
2025
|
-
|
2026
|
-
|
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
|
-
|
2030
|
-
|
2031
|
-
|
2032
|
-
|
2033
|
-
|
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
|
-
|
2037
|
-
|
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
|
-
|
2040
|
-
|
2041
|
-
|
2042
|
-
|
2043
|
-
|
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
|
-
|
2046
|
-
|
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
|
-
|
2049
|
-
|
2050
|
-
|
2221
|
+
// Now set the selection to as it was before
|
2222
|
+
range = this._createRange(
|
2223
|
+
startContainer, startOffset, endContainer, endOffset );
|
2051
2224
|
}
|
2052
|
-
|
2225
|
+
return range;
|
2226
|
+
};
|
2053
2227
|
|
2054
|
-
|
2055
|
-
|
2056
|
-
this.
|
2057
|
-
this._updatePath( range, true );
|
2228
|
+
proto._removeFormat = function ( tag, attributes, range, partial ) {
|
2229
|
+
// Add bookmark
|
2230
|
+
this._saveRangeToBookmark( range );
|
2058
2231
|
|
2059
|
-
//
|
2060
|
-
|
2061
|
-
|
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
|
-
|
2065
|
-
|
2066
|
-
|
2067
|
-
|
2068
|
-
|
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
|
-
|
2074
|
-
|
2075
|
-
|
2076
|
-
|
2077
|
-
|
2078
|
-
|
2079
|
-
|
2080
|
-
|
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
|
2084
|
-
|
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
|
-
|
2097
|
-
|
2098
|
-
|
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
|
-
|
2101
|
-
|
2102
|
-
|
2103
|
-
|
2104
|
-
|
2105
|
-
|
2106
|
-
|
2107
|
-
|
2108
|
-
|
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
|
-
//
|
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
|
-
|
2115
|
-
|
2116
|
-
|
2117
|
-
|
2118
|
-
])
|
2119
|
-
);
|
2294
|
+
for ( child = node.firstChild; child; child = next ) {
|
2295
|
+
next = child.nextSibling;
|
2296
|
+
examineNode( child, exemplar );
|
2297
|
+
}
|
2120
2298
|
}
|
2121
|
-
|
2122
|
-
|
2123
|
-
|
2124
|
-
|
2125
|
-
|
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
|
-
|
2145
|
-
|
2146
|
-
|
2147
|
-
|
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
|
-
|
2163
|
-
|
2164
|
-
|
2165
|
-
|
2166
|
-
|
2167
|
-
|
2168
|
-
|
2169
|
-
|
2170
|
-
|
2171
|
-
|
2172
|
-
|
2173
|
-
|
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
|
-
|
2188
|
-
|
2189
|
-
|
2190
|
-
|
2191
|
-
}
|
2192
|
-
|
2193
|
-
|
2194
|
-
|
2195
|
-
|
2196
|
-
|
2197
|
-
|
2198
|
-
|
2199
|
-
|
2200
|
-
|
2201
|
-
|
2202
|
-
|
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
|
-
|
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
|
-
|
2350
|
+
// Save undo checkpoint
|
2351
|
+
this._recordUndoState( range );
|
2352
|
+
this._getRangeAndRemoveBookmark( range );
|
2224
2353
|
|
2225
|
-
|
2226
|
-
|
2227
|
-
|
2228
|
-
|
2229
|
-
|
2230
|
-
|
2231
|
-
|
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
|
-
|
2258
|
-
|
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
|
-
|
2268
|
-
|
2269
|
-
|
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
|
-
|
2319
|
-
|
2320
|
-
|
2321
|
-
|
2322
|
-
|
2323
|
-
|
2324
|
-
};
|
2374
|
+
// --- Block formatting ---
|
2375
|
+
|
2376
|
+
var tagAfterSplit = {
|
2377
|
+
DT: 'DD',
|
2378
|
+
DD: 'DT',
|
2379
|
+
LI: 'LI'
|
2325
2380
|
};
|
2326
2381
|
|
2327
|
-
var
|
2328
|
-
|
2329
|
-
|
2330
|
-
|
2331
|
-
|
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
|
-
|
2334
|
-
|
2335
|
-
|
2336
|
-
|
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
|
-
|
2349
|
-
|
2350
|
-
|
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
|
-
|
2354
|
-
|
2355
|
-
|
2356
|
-
|
2357
|
-
|
2358
|
-
|
2359
|
-
|
2360
|
-
|
2361
|
-
|
2362
|
-
|
2363
|
-
|
2364
|
-
|
2365
|
-
|
2366
|
-
|
2367
|
-
|
2368
|
-
|
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
|
-
|
2421
|
-
|
2422
|
-
|
2423
|
-
|
2424
|
-
|
2425
|
-
|
2426
|
-
|
2427
|
-
|
2428
|
-
|
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
|
-
|
2440
|
+
proto.modifyBlocks = function ( modify, range ) {
|
2441
|
+
if ( !range && !( range = this.getSelection() ) ) {
|
2442
|
+
return this;
|
2443
|
+
}
|
2439
2444
|
|
2440
|
-
1.
|
2441
|
-
|
2442
|
-
|
2443
|
-
|
2444
|
-
|
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
|
-
|
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
|
2521
|
-
return
|
2522
|
-
|
2523
|
-
|
2483
|
+
var increaseBlockQuoteLevel = function ( frag ) {
|
2484
|
+
return this.createElement( 'BLOCKQUOTE',
|
2485
|
+
this._config.tagAttributes.blockquote, [
|
2486
|
+
frag
|
2487
|
+
]);
|
2524
2488
|
};
|
2525
|
-
|
2526
|
-
|
2527
|
-
|
2528
|
-
|
2529
|
-
|
2530
|
-
}
|
2531
|
-
|
2532
|
-
|
2533
|
-
|
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
|
-
|
2538
|
-
|
2539
|
-
|
2540
|
-
|
2541
|
-
|
2542
|
-
|
2543
|
-
|
2544
|
-
|
2545
|
-
|
2546
|
-
|
2547
|
-
|
2548
|
-
|
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
|
-
|
2551
|
-
|
2552
|
-
|
2553
|
-
|
2554
|
-
|
2555
|
-
|
2556
|
-
|
2557
|
-
|
2558
|
-
|
2559
|
-
|
2560
|
-
|
2561
|
-
|
2562
|
-
|
2563
|
-
|
2564
|
-
|
2565
|
-
|
2566
|
-
|
2567
|
-
|
2568
|
-
|
2569
|
-
|
2570
|
-
|
2571
|
-
|
2572
|
-
|
2573
|
-
|
2574
|
-
|
2575
|
-
|
2576
|
-
|
2577
|
-
|
2578
|
-
|
2579
|
-
|
2580
|
-
|
2581
|
-
|
2582
|
-
|
2583
|
-
|
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
|
-
|
2591
|
-
|
2592
|
-
|
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
|
-
|
2560
|
+
var makeOrderedList = function ( frag ) {
|
2561
|
+
makeList( this, frag, 'OL' );
|
2562
|
+
return frag;
|
2563
|
+
};
|
2599
2564
|
|
2600
|
-
|
2601
|
-
|
2602
|
-
|
2603
|
-
|
2604
|
-
|
2605
|
-
|
2606
|
-
|
2607
|
-
|
2608
|
-
|
2609
|
-
|
2610
|
-
|
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
|
-
|
2577
|
+
fixContainer( listFrag );
|
2578
|
+
replaceWith( list, listFrag );
|
2579
|
+
}
|
2580
|
+
return frag;
|
2615
2581
|
};
|
2616
2582
|
|
2617
|
-
|
2618
|
-
|
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
|
-
|
2621
|
-
var
|
2622
|
-
|
2623
|
-
|
2624
|
-
|
2625
|
-
|
2626
|
-
|
2627
|
-
|
2628
|
-
|
2629
|
-
|
2630
|
-
|
2631
|
-
|
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
|
-
|
2635
|
-
|
2636
|
-
}
|
2630
|
+
newParent.insertBefore( node, parent );
|
2631
|
+
node = next;
|
2637
2632
|
}
|
2638
|
-
if (
|
2639
|
-
|
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
|
-
|
2658
|
-
|
2659
|
-
|
2660
|
-
|
2661
|
-
|
2662
|
-
|
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
|
-
|
2665
|
-
self._recordUndoState( range );
|
2666
|
-
self._getRangeAndRemoveBookmark( range );
|
2646
|
+
// --- Clean ---
|
2667
2647
|
|
2668
|
-
|
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
|
-
|
2677
|
-
|
2678
|
-
|
2679
|
-
|
2680
|
-
(
|
2681
|
-
|
2682
|
-
|
2683
|
-
|
2684
|
-
|
2685
|
-
|
2686
|
-
|
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
|
-
|
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
|
-
|
2700
|
-
|
2701
|
-
|
2702
|
-
|
2703
|
-
|
2704
|
-
|
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
|
-
|
2708
|
-
|
2709
|
-
|
2710
|
-
|
2711
|
-
|
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
|
-
|
2714
|
-
|
2715
|
-
|
2716
|
-
|
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
|
-
|
2720
|
-
|
2721
|
-
|
2722
|
-
|
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
|
-
|
2727
|
-
|
2728
|
-
|
2729
|
-
|
2730
|
-
|
2731
|
-
|
2732
|
-
|
2733
|
-
|
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
|
-
|
2738
|
-
|
2773
|
+
if ( newTreeTop ) {
|
2774
|
+
newTreeBottom.appendChild( empty( span ) );
|
2775
|
+
parent.replaceChild( newTreeTop, span );
|
2776
|
+
}
|
2739
2777
|
|
2740
|
-
|
2741
|
-
|
2742
|
-
|
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
|
-
|
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
|
-
|
2748
|
-
|
2749
|
-
|
2750
|
-
|
2751
|
-
|
2752
|
-
|
2753
|
-
|
2754
|
-
|
2755
|
-
|
2756
|
-
|
2757
|
-
|
2758
|
-
|
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
|
-
|
2762
|
-
|
2763
|
-
event.preventDefault();
|
2764
|
-
self[ method ]();
|
2765
|
-
};
|
2766
|
-
};
|
2862
|
+
/*
|
2863
|
+
Two purposes:
|
2767
2864
|
|
2768
|
-
|
2769
|
-
|
2770
|
-
|
2771
|
-
|
2772
|
-
|
2773
|
-
|
2774
|
-
|
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
|
-
|
2777
|
-
|
2778
|
-
|
2779
|
-
|
2780
|
-
|
2781
|
-
|
2782
|
-
//
|
2783
|
-
//
|
2784
|
-
|
2785
|
-
|
2786
|
-
|
2787
|
-
|
2788
|
-
|
2789
|
-
|
2790
|
-
|
2791
|
-
|
2792
|
-
|
2793
|
-
|
2794
|
-
|
2795
|
-
|
2796
|
-
|
2797
|
-
|
2798
|
-
|
2799
|
-
|
2800
|
-
|
2801
|
-
|
2802
|
-
|
2803
|
-
|
2804
|
-
|
2805
|
-
|
2806
|
-
|
2807
|
-
|
2808
|
-
|
2809
|
-
|
2810
|
-
|
2811
|
-
|
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
|
-
|
2814
|
-
|
2815
|
-
|
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
|
2826
|
-
|
2827
|
-
|
2828
|
-
|
2829
|
-
|
2830
|
-
|
2831
|
-
|
2832
|
-
|
2833
|
-
|
2834
|
-
|
2835
|
-
|
2836
|
-
|
2837
|
-
|
2838
|
-
|
2839
|
-
|
2840
|
-
|
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
|
-
|
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
|
-
|
2849
|
-
|
2850
|
-
|
2851
|
-
|
2852
|
-
|
2853
|
-
|
2854
|
-
|
2855
|
-
|
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
|
-
//
|
2859
|
-
if (
|
2860
|
-
block
|
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
|
-
|
2864
|
-
//
|
2865
|
-
|
2866
|
-
|
2867
|
-
|
2868
|
-
|
2869
|
-
|
2870
|
-
|
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
|
-
|
2875
|
-
|
2876
|
-
|
2877
|
-
|
2878
|
-
|
2879
|
-
|
2880
|
-
|
2881
|
-
|
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
|
-
|
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
|
-
|
2912
|
-
|
2913
|
-
|
2914
|
-
|
2915
|
-
|
2916
|
-
|
2917
|
-
|
2918
|
-
|
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
|
-
|
2921
|
-
|
2922
|
-
self._updatePath( range, true );
|
3040
|
+
}, 0 );
|
3041
|
+
};
|
2923
3042
|
|
2924
|
-
|
2925
|
-
|
2926
|
-
|
2927
|
-
|
2928
|
-
|
2929
|
-
|
2930
|
-
|
2931
|
-
|
2932
|
-
|
2933
|
-
|
2934
|
-
|
2935
|
-
|
2936
|
-
|
2937
|
-
|
2938
|
-
|
2939
|
-
|
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
|
-
|
2973
|
-
|
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
|
-
|
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
|
-
|
3010
|
-
|
3011
|
-
|
3012
|
-
|
3013
|
-
|
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
|
-
|
3030
|
-
|
3072
|
+
/*jshint loopfunc: false */
|
3073
|
+
});
|
3074
|
+
if ( fireDrop ) {
|
3075
|
+
this.fireEvent( 'drop', {
|
3076
|
+
dataTransfer: clipboardData
|
3077
|
+
});
|
3031
3078
|
}
|
3079
|
+
return;
|
3032
3080
|
}
|
3033
|
-
|
3034
|
-
|
3035
|
-
|
3036
|
-
|
3037
|
-
|
3038
|
-
|
3039
|
-
|
3040
|
-
|
3041
|
-
|
3042
|
-
|
3043
|
-
|
3044
|
-
|
3045
|
-
|
3046
|
-
|
3047
|
-
|
3048
|
-
|
3049
|
-
|
3050
|
-
|
3051
|
-
|
3052
|
-
|
3053
|
-
|
3054
|
-
|
3055
|
-
|
3056
|
-
|
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
|
-
|
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
|
-
|
3072
|
-
|
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
|
-
|
3082
|
-
|
3083
|
-
|
3084
|
-
|
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
|
-
//
|
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
|
-
|
3107
|
-
|
3108
|
-
|
3109
|
-
|
3110
|
-
|
3111
|
-
|
3112
|
-
|
3113
|
-
|
3114
|
-
|
3115
|
-
|
3116
|
-
|
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 (
|
3160
|
-
|
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
|
-
|
3612
|
+
if ( typeof exports === 'object' ) {
|
3613
|
+
module.exports = Squire;
|
3614
|
+
} else {
|
3615
|
+
win.Squire = Squire;
|
3616
|
+
}
|
3544
3617
|
}
|
3545
3618
|
|
3546
3619
|
}( document ) );
|