godmin-medium 0.1.0
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 +7 -0
- data/.gitignore +14 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/README.md +52 -0
- data/Rakefile +2 -0
- data/app/assets/javascripts/godmin-medium/index.js +38 -0
- data/app/assets/stylesheets/godmin-medium/index.css +15 -0
- data/godmin-medium.gemspec +26 -0
- data/lib/godmin/medium/engine.rb +6 -0
- data/lib/godmin/medium/helper.rb +14 -0
- data/lib/godmin/medium/version.rb +5 -0
- data/lib/godmin/medium.rb +9 -0
- data/vendor/assets/javascripts/godmin-medium/medium-editor.js +4445 -0
- data/vendor/assets/stylesheets/godmin-medium/medium-editor.css +210 -0
- data/vendor/assets/stylesheets/godmin-medium/themes/bootstrap.css +67 -0
- data/vendor/assets/stylesheets/godmin-medium/themes/bootstrap.min.css +1 -0
- data/vendor/assets/stylesheets/godmin-medium/themes/default.css +63 -0
- data/vendor/assets/stylesheets/godmin-medium/themes/default.min.css +1 -0
- data/vendor/assets/stylesheets/godmin-medium/themes/flat.css +57 -0
- data/vendor/assets/stylesheets/godmin-medium/themes/flat.min.css +1 -0
- data/vendor/assets/stylesheets/godmin-medium/themes/mani.css +56 -0
- data/vendor/assets/stylesheets/godmin-medium/themes/mani.min.css +1 -0
- data/vendor/assets/stylesheets/godmin-medium/themes/roman.css +57 -0
- data/vendor/assets/stylesheets/godmin-medium/themes/roman.min.css +1 -0
- metadata +118 -0
@@ -0,0 +1,4445 @@
|
|
1
|
+
/*global self, document, DOMException */
|
2
|
+
|
3
|
+
/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js */
|
4
|
+
|
5
|
+
// Full polyfill for browsers with no classList support
|
6
|
+
if (!("classList" in document.createElement("_"))) {
|
7
|
+
(function (view) {
|
8
|
+
|
9
|
+
"use strict";
|
10
|
+
|
11
|
+
if (!('Element' in view)) return;
|
12
|
+
|
13
|
+
var
|
14
|
+
classListProp = "classList"
|
15
|
+
, protoProp = "prototype"
|
16
|
+
, elemCtrProto = view.Element[protoProp]
|
17
|
+
, objCtr = Object
|
18
|
+
, strTrim = String[protoProp].trim || function () {
|
19
|
+
return this.replace(/^\s+|\s+$/g, "");
|
20
|
+
}
|
21
|
+
, arrIndexOf = Array[protoProp].indexOf || function (item) {
|
22
|
+
var
|
23
|
+
i = 0
|
24
|
+
, len = this.length
|
25
|
+
;
|
26
|
+
for (; i < len; i++) {
|
27
|
+
if (i in this && this[i] === item) {
|
28
|
+
return i;
|
29
|
+
}
|
30
|
+
}
|
31
|
+
return -1;
|
32
|
+
}
|
33
|
+
// Vendors: please allow content code to instantiate DOMExceptions
|
34
|
+
, DOMEx = function (type, message) {
|
35
|
+
this.name = type;
|
36
|
+
this.code = DOMException[type];
|
37
|
+
this.message = message;
|
38
|
+
}
|
39
|
+
, checkTokenAndGetIndex = function (classList, token) {
|
40
|
+
if (token === "") {
|
41
|
+
throw new DOMEx(
|
42
|
+
"SYNTAX_ERR"
|
43
|
+
, "An invalid or illegal string was specified"
|
44
|
+
);
|
45
|
+
}
|
46
|
+
if (/\s/.test(token)) {
|
47
|
+
throw new DOMEx(
|
48
|
+
"INVALID_CHARACTER_ERR"
|
49
|
+
, "String contains an invalid character"
|
50
|
+
);
|
51
|
+
}
|
52
|
+
return arrIndexOf.call(classList, token);
|
53
|
+
}
|
54
|
+
, ClassList = function (elem) {
|
55
|
+
var
|
56
|
+
trimmedClasses = strTrim.call(elem.getAttribute("class") || "")
|
57
|
+
, classes = trimmedClasses ? trimmedClasses.split(/\s+/) : []
|
58
|
+
, i = 0
|
59
|
+
, len = classes.length
|
60
|
+
;
|
61
|
+
for (; i < len; i++) {
|
62
|
+
this.push(classes[i]);
|
63
|
+
}
|
64
|
+
this._updateClassName = function () {
|
65
|
+
elem.setAttribute("class", this.toString());
|
66
|
+
};
|
67
|
+
}
|
68
|
+
, classListProto = ClassList[protoProp] = []
|
69
|
+
, classListGetter = function () {
|
70
|
+
return new ClassList(this);
|
71
|
+
}
|
72
|
+
;
|
73
|
+
// Most DOMException implementations don't allow calling DOMException's toString()
|
74
|
+
// on non-DOMExceptions. Error's toString() is sufficient here.
|
75
|
+
DOMEx[protoProp] = Error[protoProp];
|
76
|
+
classListProto.item = function (i) {
|
77
|
+
return this[i] || null;
|
78
|
+
};
|
79
|
+
classListProto.contains = function (token) {
|
80
|
+
token += "";
|
81
|
+
return checkTokenAndGetIndex(this, token) !== -1;
|
82
|
+
};
|
83
|
+
classListProto.add = function () {
|
84
|
+
var
|
85
|
+
tokens = arguments
|
86
|
+
, i = 0
|
87
|
+
, l = tokens.length
|
88
|
+
, token
|
89
|
+
, updated = false
|
90
|
+
;
|
91
|
+
do {
|
92
|
+
token = tokens[i] + "";
|
93
|
+
if (checkTokenAndGetIndex(this, token) === -1) {
|
94
|
+
this.push(token);
|
95
|
+
updated = true;
|
96
|
+
}
|
97
|
+
}
|
98
|
+
while (++i < l);
|
99
|
+
|
100
|
+
if (updated) {
|
101
|
+
this._updateClassName();
|
102
|
+
}
|
103
|
+
};
|
104
|
+
classListProto.remove = function () {
|
105
|
+
var
|
106
|
+
tokens = arguments
|
107
|
+
, i = 0
|
108
|
+
, l = tokens.length
|
109
|
+
, token
|
110
|
+
, updated = false
|
111
|
+
, index
|
112
|
+
;
|
113
|
+
do {
|
114
|
+
token = tokens[i] + "";
|
115
|
+
index = checkTokenAndGetIndex(this, token);
|
116
|
+
while (index !== -1) {
|
117
|
+
this.splice(index, 1);
|
118
|
+
updated = true;
|
119
|
+
index = checkTokenAndGetIndex(this, token);
|
120
|
+
}
|
121
|
+
}
|
122
|
+
while (++i < l);
|
123
|
+
|
124
|
+
if (updated) {
|
125
|
+
this._updateClassName();
|
126
|
+
}
|
127
|
+
};
|
128
|
+
classListProto.toggle = function (token, force) {
|
129
|
+
token += "";
|
130
|
+
|
131
|
+
var
|
132
|
+
result = this.contains(token)
|
133
|
+
, method = result ?
|
134
|
+
force !== true && "remove"
|
135
|
+
:
|
136
|
+
force !== false && "add"
|
137
|
+
;
|
138
|
+
|
139
|
+
if (method) {
|
140
|
+
this[method](token);
|
141
|
+
}
|
142
|
+
|
143
|
+
if (force === true || force === false) {
|
144
|
+
return force;
|
145
|
+
} else {
|
146
|
+
return !result;
|
147
|
+
}
|
148
|
+
};
|
149
|
+
classListProto.toString = function () {
|
150
|
+
return this.join(" ");
|
151
|
+
};
|
152
|
+
|
153
|
+
if (objCtr.defineProperty) {
|
154
|
+
var classListPropDesc = {
|
155
|
+
get: classListGetter
|
156
|
+
, enumerable: true
|
157
|
+
, configurable: true
|
158
|
+
};
|
159
|
+
try {
|
160
|
+
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
|
161
|
+
} catch (ex) { // IE 8 doesn't support enumerable:true
|
162
|
+
if (ex.number === -0x7FF5EC54) {
|
163
|
+
classListPropDesc.enumerable = false;
|
164
|
+
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
|
165
|
+
}
|
166
|
+
}
|
167
|
+
} else if (objCtr[protoProp].__defineGetter__) {
|
168
|
+
elemCtrProto.__defineGetter__(classListProp, classListGetter);
|
169
|
+
}
|
170
|
+
|
171
|
+
}(self));
|
172
|
+
}
|
173
|
+
|
174
|
+
/* Blob.js
|
175
|
+
* A Blob implementation.
|
176
|
+
* 2014-07-24
|
177
|
+
*
|
178
|
+
* By Eli Grey, http://eligrey.com
|
179
|
+
* By Devin Samarin, https://github.com/dsamarin
|
180
|
+
* License: X11/MIT
|
181
|
+
* See https://github.com/eligrey/Blob.js/blob/master/LICENSE.md
|
182
|
+
*/
|
183
|
+
|
184
|
+
/*global self, unescape */
|
185
|
+
/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
|
186
|
+
plusplus: true */
|
187
|
+
|
188
|
+
/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */
|
189
|
+
|
190
|
+
(function (view) {
|
191
|
+
"use strict";
|
192
|
+
|
193
|
+
view.URL = view.URL || view.webkitURL;
|
194
|
+
|
195
|
+
if (view.Blob && view.URL) {
|
196
|
+
try {
|
197
|
+
new Blob;
|
198
|
+
return;
|
199
|
+
} catch (e) {}
|
200
|
+
}
|
201
|
+
|
202
|
+
// Internally we use a BlobBuilder implementation to base Blob off of
|
203
|
+
// in order to support older browsers that only have BlobBuilder
|
204
|
+
var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) {
|
205
|
+
var
|
206
|
+
get_class = function(object) {
|
207
|
+
return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1];
|
208
|
+
}
|
209
|
+
, FakeBlobBuilder = function BlobBuilder() {
|
210
|
+
this.data = [];
|
211
|
+
}
|
212
|
+
, FakeBlob = function Blob(data, type, encoding) {
|
213
|
+
this.data = data;
|
214
|
+
this.size = data.length;
|
215
|
+
this.type = type;
|
216
|
+
this.encoding = encoding;
|
217
|
+
}
|
218
|
+
, FBB_proto = FakeBlobBuilder.prototype
|
219
|
+
, FB_proto = FakeBlob.prototype
|
220
|
+
, FileReaderSync = view.FileReaderSync
|
221
|
+
, FileException = function(type) {
|
222
|
+
this.code = this[this.name = type];
|
223
|
+
}
|
224
|
+
, file_ex_codes = (
|
225
|
+
"NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR "
|
226
|
+
+ "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR"
|
227
|
+
).split(" ")
|
228
|
+
, file_ex_code = file_ex_codes.length
|
229
|
+
, real_URL = view.URL || view.webkitURL || view
|
230
|
+
, real_create_object_URL = real_URL.createObjectURL
|
231
|
+
, real_revoke_object_URL = real_URL.revokeObjectURL
|
232
|
+
, URL = real_URL
|
233
|
+
, btoa = view.btoa
|
234
|
+
, atob = view.atob
|
235
|
+
|
236
|
+
, ArrayBuffer = view.ArrayBuffer
|
237
|
+
, Uint8Array = view.Uint8Array
|
238
|
+
|
239
|
+
, origin = /^[\w-]+:\/*\[?[\w\.:-]+\]?(?::[0-9]+)?/
|
240
|
+
;
|
241
|
+
FakeBlob.fake = FB_proto.fake = true;
|
242
|
+
while (file_ex_code--) {
|
243
|
+
FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;
|
244
|
+
}
|
245
|
+
// Polyfill URL
|
246
|
+
if (!real_URL.createObjectURL) {
|
247
|
+
URL = view.URL = function(uri) {
|
248
|
+
var
|
249
|
+
uri_info = document.createElementNS("http://www.w3.org/1999/xhtml", "a")
|
250
|
+
, uri_origin
|
251
|
+
;
|
252
|
+
uri_info.href = uri;
|
253
|
+
if (!("origin" in uri_info)) {
|
254
|
+
if (uri_info.protocol.toLowerCase() === "data:") {
|
255
|
+
uri_info.origin = null;
|
256
|
+
} else {
|
257
|
+
uri_origin = uri.match(origin);
|
258
|
+
uri_info.origin = uri_origin && uri_origin[1];
|
259
|
+
}
|
260
|
+
}
|
261
|
+
return uri_info;
|
262
|
+
};
|
263
|
+
}
|
264
|
+
URL.createObjectURL = function(blob) {
|
265
|
+
var
|
266
|
+
type = blob.type
|
267
|
+
, data_URI_header
|
268
|
+
;
|
269
|
+
if (type === null) {
|
270
|
+
type = "application/octet-stream";
|
271
|
+
}
|
272
|
+
if (blob instanceof FakeBlob) {
|
273
|
+
data_URI_header = "data:" + type;
|
274
|
+
if (blob.encoding === "base64") {
|
275
|
+
return data_URI_header + ";base64," + blob.data;
|
276
|
+
} else if (blob.encoding === "URI") {
|
277
|
+
return data_URI_header + "," + decodeURIComponent(blob.data);
|
278
|
+
} if (btoa) {
|
279
|
+
return data_URI_header + ";base64," + btoa(blob.data);
|
280
|
+
} else {
|
281
|
+
return data_URI_header + "," + encodeURIComponent(blob.data);
|
282
|
+
}
|
283
|
+
} else if (real_create_object_URL) {
|
284
|
+
return real_create_object_URL.call(real_URL, blob);
|
285
|
+
}
|
286
|
+
};
|
287
|
+
URL.revokeObjectURL = function(object_URL) {
|
288
|
+
if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) {
|
289
|
+
real_revoke_object_URL.call(real_URL, object_URL);
|
290
|
+
}
|
291
|
+
};
|
292
|
+
FBB_proto.append = function(data/*, endings*/) {
|
293
|
+
var bb = this.data;
|
294
|
+
// decode data to a binary string
|
295
|
+
if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {
|
296
|
+
var
|
297
|
+
str = ""
|
298
|
+
, buf = new Uint8Array(data)
|
299
|
+
, i = 0
|
300
|
+
, buf_len = buf.length
|
301
|
+
;
|
302
|
+
for (; i < buf_len; i++) {
|
303
|
+
str += String.fromCharCode(buf[i]);
|
304
|
+
}
|
305
|
+
bb.push(str);
|
306
|
+
} else if (get_class(data) === "Blob" || get_class(data) === "File") {
|
307
|
+
if (FileReaderSync) {
|
308
|
+
var fr = new FileReaderSync;
|
309
|
+
bb.push(fr.readAsBinaryString(data));
|
310
|
+
} else {
|
311
|
+
// async FileReader won't work as BlobBuilder is sync
|
312
|
+
throw new FileException("NOT_READABLE_ERR");
|
313
|
+
}
|
314
|
+
} else if (data instanceof FakeBlob) {
|
315
|
+
if (data.encoding === "base64" && atob) {
|
316
|
+
bb.push(atob(data.data));
|
317
|
+
} else if (data.encoding === "URI") {
|
318
|
+
bb.push(decodeURIComponent(data.data));
|
319
|
+
} else if (data.encoding === "raw") {
|
320
|
+
bb.push(data.data);
|
321
|
+
}
|
322
|
+
} else {
|
323
|
+
if (typeof data !== "string") {
|
324
|
+
data += ""; // convert unsupported types to strings
|
325
|
+
}
|
326
|
+
// decode UTF-16 to binary string
|
327
|
+
bb.push(unescape(encodeURIComponent(data)));
|
328
|
+
}
|
329
|
+
};
|
330
|
+
FBB_proto.getBlob = function(type) {
|
331
|
+
if (!arguments.length) {
|
332
|
+
type = null;
|
333
|
+
}
|
334
|
+
return new FakeBlob(this.data.join(""), type, "raw");
|
335
|
+
};
|
336
|
+
FBB_proto.toString = function() {
|
337
|
+
return "[object BlobBuilder]";
|
338
|
+
};
|
339
|
+
FB_proto.slice = function(start, end, type) {
|
340
|
+
var args = arguments.length;
|
341
|
+
if (args < 3) {
|
342
|
+
type = null;
|
343
|
+
}
|
344
|
+
return new FakeBlob(
|
345
|
+
this.data.slice(start, args > 1 ? end : this.data.length)
|
346
|
+
, type
|
347
|
+
, this.encoding
|
348
|
+
);
|
349
|
+
};
|
350
|
+
FB_proto.toString = function() {
|
351
|
+
return "[object Blob]";
|
352
|
+
};
|
353
|
+
FB_proto.close = function() {
|
354
|
+
this.size = 0;
|
355
|
+
delete this.data;
|
356
|
+
};
|
357
|
+
return FakeBlobBuilder;
|
358
|
+
}(view));
|
359
|
+
|
360
|
+
view.Blob = function(blobParts, options) {
|
361
|
+
var type = options ? (options.type || "") : "";
|
362
|
+
var builder = new BlobBuilder();
|
363
|
+
if (blobParts) {
|
364
|
+
for (var i = 0, len = blobParts.length; i < len; i++) {
|
365
|
+
if (Uint8Array && blobParts[i] instanceof Uint8Array) {
|
366
|
+
builder.append(blobParts[i].buffer);
|
367
|
+
}
|
368
|
+
else {
|
369
|
+
builder.append(blobParts[i]);
|
370
|
+
}
|
371
|
+
}
|
372
|
+
}
|
373
|
+
var blob = builder.getBlob(type);
|
374
|
+
if (!blob.slice && blob.webkitSlice) {
|
375
|
+
blob.slice = blob.webkitSlice;
|
376
|
+
}
|
377
|
+
return blob;
|
378
|
+
};
|
379
|
+
|
380
|
+
var getPrototypeOf = Object.getPrototypeOf || function(object) {
|
381
|
+
return object.__proto__;
|
382
|
+
};
|
383
|
+
view.Blob.prototype = getPrototypeOf(new view.Blob());
|
384
|
+
}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this));
|
385
|
+
|
386
|
+
(function (root, factory) {
|
387
|
+
'use strict';
|
388
|
+
if (typeof module === 'object') {
|
389
|
+
module.exports = factory;
|
390
|
+
} else if (typeof define === 'function' && define.amd) {
|
391
|
+
define(function () {
|
392
|
+
return factory;
|
393
|
+
});
|
394
|
+
} else {
|
395
|
+
root.MediumEditor = factory;
|
396
|
+
}
|
397
|
+
}(this, function () {
|
398
|
+
|
399
|
+
'use strict';
|
400
|
+
|
401
|
+
var Util;
|
402
|
+
|
403
|
+
(function (window) {
|
404
|
+
'use strict';
|
405
|
+
|
406
|
+
// Params: Array, Boolean, Object
|
407
|
+
function getProp(parts, create, context) {
|
408
|
+
if (!context) {
|
409
|
+
context = window;
|
410
|
+
}
|
411
|
+
|
412
|
+
try {
|
413
|
+
for (var i = 0; i < parts.length; i++) {
|
414
|
+
var p = parts[i];
|
415
|
+
if (!(p in context)) {
|
416
|
+
if (create) {
|
417
|
+
context[p] = {};
|
418
|
+
} else {
|
419
|
+
return;
|
420
|
+
}
|
421
|
+
}
|
422
|
+
context = context[p];
|
423
|
+
}
|
424
|
+
return context;
|
425
|
+
} catch (e) {
|
426
|
+
// "p in context" throws an exception when context is a number, boolean, etc. rather than an object,
|
427
|
+
// so in that corner case just return undefined (by having no return statement)
|
428
|
+
}
|
429
|
+
}
|
430
|
+
|
431
|
+
function copyInto(overwrite, dest) {
|
432
|
+
var prop,
|
433
|
+
sources = Array.prototype.slice.call(arguments, 2);
|
434
|
+
dest = dest || {};
|
435
|
+
for (var i = 0; i < sources.length; i++) {
|
436
|
+
var source = sources[i];
|
437
|
+
if (source) {
|
438
|
+
for (prop in source) {
|
439
|
+
if (source.hasOwnProperty(prop) &&
|
440
|
+
typeof source[prop] !== 'undefined' &&
|
441
|
+
(overwrite || dest.hasOwnProperty(prop) === false)) {
|
442
|
+
dest[prop] = source[prop];
|
443
|
+
}
|
444
|
+
}
|
445
|
+
}
|
446
|
+
}
|
447
|
+
return dest;
|
448
|
+
}
|
449
|
+
|
450
|
+
Util = {
|
451
|
+
|
452
|
+
// http://stackoverflow.com/questions/17907445/how-to-detect-ie11#comment30165888_17907562
|
453
|
+
// by rg89
|
454
|
+
isIE: ((navigator.appName === 'Microsoft Internet Explorer') || ((navigator.appName === 'Netscape') && (new RegExp('Trident/.*rv:([0-9]{1,}[.0-9]{0,})').exec(navigator.userAgent) !== null))),
|
455
|
+
|
456
|
+
// https://github.com/jashkenas/underscore
|
457
|
+
keyCode: {
|
458
|
+
BACKSPACE: 8,
|
459
|
+
TAB: 9,
|
460
|
+
ENTER: 13,
|
461
|
+
ESCAPE: 27,
|
462
|
+
SPACE: 32,
|
463
|
+
DELETE: 46
|
464
|
+
},
|
465
|
+
|
466
|
+
parentElements: ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre'],
|
467
|
+
|
468
|
+
extend: function extend(/* dest, source1, source2, ...*/) {
|
469
|
+
var args = [true].concat(Array.prototype.slice.call(arguments));
|
470
|
+
return copyInto.apply(this, args);
|
471
|
+
},
|
472
|
+
|
473
|
+
defaults: function defaults(/*dest, source1, source2, ...*/) {
|
474
|
+
var args = [false].concat(Array.prototype.slice.call(arguments));
|
475
|
+
return copyInto.apply(this, args);
|
476
|
+
},
|
477
|
+
|
478
|
+
derives: function derives(base, derived) {
|
479
|
+
var origPrototype = derived.prototype;
|
480
|
+
function Proto() { }
|
481
|
+
Proto.prototype = base.prototype;
|
482
|
+
derived.prototype = new Proto();
|
483
|
+
derived.prototype.constructor = base;
|
484
|
+
derived.prototype = copyInto(false, derived.prototype, origPrototype);
|
485
|
+
return derived;
|
486
|
+
},
|
487
|
+
|
488
|
+
// Find the next node in the DOM tree that represents any text that is being
|
489
|
+
// displayed directly next to the targetNode (passed as an argument)
|
490
|
+
// Text that appears directly next to the current node can be:
|
491
|
+
// - A sibling text node
|
492
|
+
// - A descendant of a sibling element
|
493
|
+
// - A sibling text node of an ancestor
|
494
|
+
// - A descendant of a sibling element of an ancestor
|
495
|
+
findAdjacentTextNodeWithContent: function findAdjacentTextNodeWithContent(rootNode, targetNode, ownerDocument) {
|
496
|
+
var pastTarget = false,
|
497
|
+
nextNode,
|
498
|
+
nodeIterator = ownerDocument.createNodeIterator(rootNode, NodeFilter.SHOW_TEXT, null, false);
|
499
|
+
|
500
|
+
// Use a native NodeIterator to iterate over all the text nodes that are descendants
|
501
|
+
// of the rootNode. Once past the targetNode, choose the first non-empty text node
|
502
|
+
nextNode = nodeIterator.nextNode();
|
503
|
+
while (nextNode) {
|
504
|
+
if (nextNode === targetNode) {
|
505
|
+
pastTarget = true;
|
506
|
+
} else if (pastTarget) {
|
507
|
+
if (nextNode.nodeType === 3 && nextNode.nodeValue && nextNode.nodeValue.trim().length > 0) {
|
508
|
+
break;
|
509
|
+
}
|
510
|
+
}
|
511
|
+
nextNode = nodeIterator.nextNode();
|
512
|
+
}
|
513
|
+
|
514
|
+
return nextNode;
|
515
|
+
},
|
516
|
+
|
517
|
+
isDescendant: function isDescendant(parent, child, checkEquality) {
|
518
|
+
if (!parent || !child) {
|
519
|
+
return false;
|
520
|
+
}
|
521
|
+
if (checkEquality && parent === child) {
|
522
|
+
return true;
|
523
|
+
}
|
524
|
+
var node = child.parentNode;
|
525
|
+
while (node !== null) {
|
526
|
+
if (node === parent) {
|
527
|
+
return true;
|
528
|
+
}
|
529
|
+
node = node.parentNode;
|
530
|
+
}
|
531
|
+
return false;
|
532
|
+
},
|
533
|
+
|
534
|
+
// https://github.com/jashkenas/underscore
|
535
|
+
isElement: function isElement(obj) {
|
536
|
+
return !!(obj && obj.nodeType === 1);
|
537
|
+
},
|
538
|
+
|
539
|
+
now: Date.now,
|
540
|
+
|
541
|
+
// https://github.com/jashkenas/underscore
|
542
|
+
throttle: function (func, wait) {
|
543
|
+
var THROTTLE_INTERVAL = 50,
|
544
|
+
context,
|
545
|
+
args,
|
546
|
+
result,
|
547
|
+
timeout = null,
|
548
|
+
previous = 0,
|
549
|
+
later = function () {
|
550
|
+
previous = Date.now();
|
551
|
+
timeout = null;
|
552
|
+
result = func.apply(context, args);
|
553
|
+
if (!timeout) {
|
554
|
+
context = args = null;
|
555
|
+
}
|
556
|
+
};
|
557
|
+
|
558
|
+
if (!wait && wait !== 0) {
|
559
|
+
wait = THROTTLE_INTERVAL;
|
560
|
+
}
|
561
|
+
|
562
|
+
return function () {
|
563
|
+
var now = Date.now(),
|
564
|
+
remaining = wait - (now - previous);
|
565
|
+
|
566
|
+
context = this;
|
567
|
+
args = arguments;
|
568
|
+
if (remaining <= 0 || remaining > wait) {
|
569
|
+
if (timeout) {
|
570
|
+
clearTimeout(timeout);
|
571
|
+
timeout = null;
|
572
|
+
}
|
573
|
+
previous = now;
|
574
|
+
result = func.apply(context, args);
|
575
|
+
if (!timeout) {
|
576
|
+
context = args = null;
|
577
|
+
}
|
578
|
+
} else if (!timeout) {
|
579
|
+
timeout = setTimeout(later, remaining);
|
580
|
+
}
|
581
|
+
return result;
|
582
|
+
};
|
583
|
+
},
|
584
|
+
|
585
|
+
traverseUp: function (current, testElementFunction) {
|
586
|
+
if (!current) {
|
587
|
+
return false;
|
588
|
+
}
|
589
|
+
|
590
|
+
do {
|
591
|
+
if (current.nodeType === 1) {
|
592
|
+
if (testElementFunction(current)) {
|
593
|
+
return current;
|
594
|
+
}
|
595
|
+
// do not traverse upwards past the nearest containing editor
|
596
|
+
if (current.getAttribute('data-medium-element')) {
|
597
|
+
return false;
|
598
|
+
}
|
599
|
+
}
|
600
|
+
|
601
|
+
current = current.parentNode;
|
602
|
+
} while (current);
|
603
|
+
|
604
|
+
return false;
|
605
|
+
},
|
606
|
+
|
607
|
+
htmlEntities: function (str) {
|
608
|
+
// converts special characters (like <) into their escaped/encoded values (like <).
|
609
|
+
// This allows you to show to display the string without the browser reading it as HTML.
|
610
|
+
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
611
|
+
},
|
612
|
+
|
613
|
+
// http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div
|
614
|
+
insertHTMLCommand: function (doc, html) {
|
615
|
+
var selection, range, el, fragment, node, lastNode, toReplace;
|
616
|
+
|
617
|
+
if (doc.queryCommandSupported('insertHTML')) {
|
618
|
+
try {
|
619
|
+
return doc.execCommand('insertHTML', false, html);
|
620
|
+
} catch (ignore) {}
|
621
|
+
}
|
622
|
+
|
623
|
+
selection = doc.defaultView.getSelection();
|
624
|
+
if (selection.getRangeAt && selection.rangeCount) {
|
625
|
+
range = selection.getRangeAt(0);
|
626
|
+
toReplace = range.commonAncestorContainer;
|
627
|
+
// Ensure range covers maximum amount of nodes as possible
|
628
|
+
// By moving up the DOM and selecting ancestors whose only child is the range
|
629
|
+
if ((toReplace.nodeType === 3 && toReplace.nodeValue === range.toString()) ||
|
630
|
+
(toReplace.nodeType !== 3 && toReplace.innerHTML === range.toString())) {
|
631
|
+
while (toReplace.parentNode &&
|
632
|
+
toReplace.parentNode.childNodes.length === 1 &&
|
633
|
+
!toReplace.parentNode.getAttribute('data-medium-element')) {
|
634
|
+
toReplace = toReplace.parentNode;
|
635
|
+
}
|
636
|
+
range.selectNode(toReplace);
|
637
|
+
}
|
638
|
+
range.deleteContents();
|
639
|
+
|
640
|
+
el = doc.createElement("div");
|
641
|
+
el.innerHTML = html;
|
642
|
+
fragment = doc.createDocumentFragment();
|
643
|
+
while (el.firstChild) {
|
644
|
+
node = el.firstChild;
|
645
|
+
lastNode = fragment.appendChild(node);
|
646
|
+
}
|
647
|
+
range.insertNode(fragment);
|
648
|
+
|
649
|
+
// Preserve the selection:
|
650
|
+
if (lastNode) {
|
651
|
+
range = range.cloneRange();
|
652
|
+
range.setStartAfter(lastNode);
|
653
|
+
range.collapse(true);
|
654
|
+
selection.removeAllRanges();
|
655
|
+
selection.addRange(range);
|
656
|
+
}
|
657
|
+
}
|
658
|
+
},
|
659
|
+
|
660
|
+
getSelectionRange: function (ownerDocument) {
|
661
|
+
var selection = ownerDocument.getSelection();
|
662
|
+
if (selection.rangeCount === 0) {
|
663
|
+
return null;
|
664
|
+
}
|
665
|
+
return selection.getRangeAt(0);
|
666
|
+
},
|
667
|
+
|
668
|
+
// http://stackoverflow.com/questions/1197401/how-can-i-get-the-element-the-caret-is-in-with-javascript-when-using-contentedi
|
669
|
+
// by You
|
670
|
+
getSelectionStart: function (ownerDocument) {
|
671
|
+
var node = ownerDocument.getSelection().anchorNode,
|
672
|
+
startNode = (node && node.nodeType === 3 ? node.parentNode : node);
|
673
|
+
return startNode;
|
674
|
+
},
|
675
|
+
|
676
|
+
getSelectionData: function (el) {
|
677
|
+
var tagName;
|
678
|
+
|
679
|
+
if (el && el.tagName) {
|
680
|
+
tagName = el.tagName.toLowerCase();
|
681
|
+
}
|
682
|
+
|
683
|
+
while (el && this.parentElements.indexOf(tagName) === -1) {
|
684
|
+
el = el.parentNode;
|
685
|
+
if (el && el.tagName) {
|
686
|
+
tagName = el.tagName.toLowerCase();
|
687
|
+
}
|
688
|
+
}
|
689
|
+
|
690
|
+
return {
|
691
|
+
el: el,
|
692
|
+
tagName: tagName
|
693
|
+
};
|
694
|
+
},
|
695
|
+
|
696
|
+
execFormatBlock: function (doc, tagName) {
|
697
|
+
var selectionData = this.getSelectionData(this.getSelectionStart(doc));
|
698
|
+
// FF handles blockquote differently on formatBlock
|
699
|
+
// allowing nesting, we need to use outdent
|
700
|
+
// https://developer.mozilla.org/en-US/docs/Rich-Text_Editing_in_Mozilla
|
701
|
+
if (tagName === 'blockquote' && selectionData.el &&
|
702
|
+
selectionData.el.parentNode.tagName.toLowerCase() === 'blockquote') {
|
703
|
+
return doc.execCommand('outdent', false, null);
|
704
|
+
}
|
705
|
+
if (selectionData.tagName === tagName) {
|
706
|
+
tagName = 'p';
|
707
|
+
}
|
708
|
+
// When IE we need to add <> to heading elements and
|
709
|
+
// blockquote needs to be called as indent
|
710
|
+
// http://stackoverflow.com/questions/10741831/execcommand-formatblock-headings-in-ie
|
711
|
+
// http://stackoverflow.com/questions/1816223/rich-text-editor-with-blockquote-function/1821777#1821777
|
712
|
+
if (this.isIE) {
|
713
|
+
if (tagName === 'blockquote') {
|
714
|
+
return doc.execCommand('indent', false, tagName);
|
715
|
+
}
|
716
|
+
tagName = '<' + tagName + '>';
|
717
|
+
}
|
718
|
+
return doc.execCommand('formatBlock', false, tagName);
|
719
|
+
},
|
720
|
+
|
721
|
+
// TODO: not sure if this should be here
|
722
|
+
setTargetBlank: function (el) {
|
723
|
+
var i;
|
724
|
+
if (el.tagName.toLowerCase() === 'a') {
|
725
|
+
el.target = '_blank';
|
726
|
+
} else {
|
727
|
+
el = el.getElementsByTagName('a');
|
728
|
+
|
729
|
+
for (i = 0; i < el.length; i += 1) {
|
730
|
+
el[i].target = '_blank';
|
731
|
+
}
|
732
|
+
}
|
733
|
+
},
|
734
|
+
|
735
|
+
addClassToAnchors: function (el, buttonClass) {
|
736
|
+
var classes = buttonClass.split(' '),
|
737
|
+
i,
|
738
|
+
j;
|
739
|
+
if (el.tagName.toLowerCase() === 'a') {
|
740
|
+
for (j = 0; j < classes.length; j += 1) {
|
741
|
+
el.classList.add(classes[j]);
|
742
|
+
}
|
743
|
+
} else {
|
744
|
+
el = el.getElementsByTagName('a');
|
745
|
+
for (i = 0; i < el.length; i += 1) {
|
746
|
+
for (j = 0; j < classes.length; j += 1) {
|
747
|
+
el[i].classList.add(classes[j]);
|
748
|
+
}
|
749
|
+
}
|
750
|
+
}
|
751
|
+
},
|
752
|
+
|
753
|
+
isListItem: function (node) {
|
754
|
+
if (!node) {
|
755
|
+
return false;
|
756
|
+
}
|
757
|
+
if (node.tagName.toLowerCase() === 'li') {
|
758
|
+
return true;
|
759
|
+
}
|
760
|
+
|
761
|
+
var parentNode = node.parentNode,
|
762
|
+
tagName = parentNode.tagName.toLowerCase();
|
763
|
+
while (this.parentElements.indexOf(tagName) === -1 && tagName !== 'div') {
|
764
|
+
if (tagName === 'li') {
|
765
|
+
return true;
|
766
|
+
}
|
767
|
+
parentNode = parentNode.parentNode;
|
768
|
+
if (parentNode && parentNode.tagName) {
|
769
|
+
tagName = parentNode.tagName.toLowerCase();
|
770
|
+
} else {
|
771
|
+
return false;
|
772
|
+
}
|
773
|
+
}
|
774
|
+
return false;
|
775
|
+
},
|
776
|
+
|
777
|
+
cleanListDOM: function (element) {
|
778
|
+
if (element.tagName.toLowerCase() === 'li') {
|
779
|
+
var list = element.parentElement;
|
780
|
+
if (list.parentElement.tagName.toLowerCase() === 'p') { // yes we need to clean up
|
781
|
+
this.unwrapElement(list.parentElement);
|
782
|
+
}
|
783
|
+
}
|
784
|
+
},
|
785
|
+
|
786
|
+
unwrapElement: function (element) {
|
787
|
+
var parent = element.parentNode,
|
788
|
+
current = element.firstChild,
|
789
|
+
next;
|
790
|
+
do {
|
791
|
+
next = current.nextSibling;
|
792
|
+
parent.insertBefore(current, element);
|
793
|
+
current = next;
|
794
|
+
} while (current);
|
795
|
+
parent.removeChild(element);
|
796
|
+
},
|
797
|
+
|
798
|
+
warn: function(){
|
799
|
+
if(window.console !== undefined && typeof window.console.warn === 'function'){
|
800
|
+
window.console.warn.apply(console, arguments);
|
801
|
+
}
|
802
|
+
},
|
803
|
+
|
804
|
+
deprecated: function(oldName, newName, version){
|
805
|
+
// simple deprecation warning mechanism.
|
806
|
+
var m = oldName + " is deprecated, please use " + newName + " instead.";
|
807
|
+
if(version){
|
808
|
+
m += " Will be removed in " + version;
|
809
|
+
}
|
810
|
+
Util.warn(m);
|
811
|
+
},
|
812
|
+
|
813
|
+
deprecatedMethod: function (oldName, newName, args, version) {
|
814
|
+
// run the replacement and warn when someone calls a deprecated method
|
815
|
+
Util.deprecated(oldName, newName, version);
|
816
|
+
if (typeof this[newName] === 'function') {
|
817
|
+
this[newName].apply(this, args);
|
818
|
+
}
|
819
|
+
},
|
820
|
+
|
821
|
+
cleanupAttrs: function (el, attrs) {
|
822
|
+
attrs.forEach(function (attr) {
|
823
|
+
el.removeAttribute(attr);
|
824
|
+
});
|
825
|
+
},
|
826
|
+
|
827
|
+
cleanupTags: function (el, tags) {
|
828
|
+
tags.forEach(function (tag) {
|
829
|
+
if (el.tagName.toLowerCase() === tag) {
|
830
|
+
el.parentNode.removeChild(el);
|
831
|
+
}
|
832
|
+
}, this);
|
833
|
+
},
|
834
|
+
|
835
|
+
getClosestTag : function(el, tag) { // get the closest parent
|
836
|
+
return Util.traverseUp(el, function (element) {
|
837
|
+
return element.tagName.toLowerCase() === tag.toLowerCase();
|
838
|
+
});
|
839
|
+
},
|
840
|
+
|
841
|
+
unwrap: function (el, doc) {
|
842
|
+
var fragment = doc.createDocumentFragment();
|
843
|
+
|
844
|
+
for (var i = 0; i < el.childNodes.length; i++) {
|
845
|
+
fragment.appendChild(el.childNodes[i]);
|
846
|
+
}
|
847
|
+
|
848
|
+
if (fragment.childNodes.length) {
|
849
|
+
el.parentNode.replaceChild(fragment, el);
|
850
|
+
} else {
|
851
|
+
el.parentNode.removeChild(el);
|
852
|
+
}
|
853
|
+
},
|
854
|
+
|
855
|
+
setObject: function(name, value, context){
|
856
|
+
// summary:
|
857
|
+
// Set a property from a dot-separated string, such as "A.B.C"
|
858
|
+
var parts = name.split("."), p = parts.pop(), obj = getProp(parts, true, context);
|
859
|
+
return obj && p ? (obj[p] = value) : undefined; // Object
|
860
|
+
},
|
861
|
+
|
862
|
+
getObject: function(name, create, context){
|
863
|
+
// summary:
|
864
|
+
// Get a property from a dot-separated string, such as "A.B.C"
|
865
|
+
return getProp(name ? name.split(".") : [], create, context); // Object
|
866
|
+
}
|
867
|
+
|
868
|
+
};
|
869
|
+
}(window));
|
870
|
+
|
871
|
+
var ButtonsData;
|
872
|
+
(function(){
|
873
|
+
'use strict';
|
874
|
+
|
875
|
+
ButtonsData = {
|
876
|
+
'bold': {
|
877
|
+
name: 'bold',
|
878
|
+
action: 'bold',
|
879
|
+
aria: 'bold',
|
880
|
+
tagNames: ['b', 'strong'],
|
881
|
+
style: {
|
882
|
+
prop: 'font-weight',
|
883
|
+
value: '700|bold'
|
884
|
+
},
|
885
|
+
useQueryState: true,
|
886
|
+
contentDefault: '<b>B</b>',
|
887
|
+
contentFA: '<i class="fa fa-bold"></i>',
|
888
|
+
key: 'b'
|
889
|
+
},
|
890
|
+
'italic': {
|
891
|
+
name: 'italic',
|
892
|
+
action: 'italic',
|
893
|
+
aria: 'italic',
|
894
|
+
tagNames: ['i', 'em'],
|
895
|
+
style: {
|
896
|
+
prop: 'font-style',
|
897
|
+
value: 'italic'
|
898
|
+
},
|
899
|
+
useQueryState: true,
|
900
|
+
contentDefault: '<b><i>I</i></b>',
|
901
|
+
contentFA: '<i class="fa fa-italic"></i>',
|
902
|
+
key: 'i'
|
903
|
+
},
|
904
|
+
'underline': {
|
905
|
+
name: 'underline',
|
906
|
+
action: 'underline',
|
907
|
+
aria: 'underline',
|
908
|
+
tagNames: ['u'],
|
909
|
+
style: {
|
910
|
+
prop: 'text-decoration',
|
911
|
+
value: 'underline'
|
912
|
+
},
|
913
|
+
useQueryState: true,
|
914
|
+
contentDefault: '<b><u>U</u></b>',
|
915
|
+
contentFA: '<i class="fa fa-underline"></i>',
|
916
|
+
key: 'u'
|
917
|
+
},
|
918
|
+
'strikethrough': {
|
919
|
+
name: 'strikethrough',
|
920
|
+
action: 'strikethrough',
|
921
|
+
aria: 'strike through',
|
922
|
+
tagNames: ['strike'],
|
923
|
+
style: {
|
924
|
+
prop: 'text-decoration',
|
925
|
+
value: 'line-through'
|
926
|
+
},
|
927
|
+
useQueryState: true,
|
928
|
+
contentDefault: '<s>A</s>',
|
929
|
+
contentFA: '<i class="fa fa-strikethrough"></i>'
|
930
|
+
},
|
931
|
+
'superscript': {
|
932
|
+
name: 'superscript',
|
933
|
+
action: 'superscript',
|
934
|
+
aria: 'superscript',
|
935
|
+
tagNames: ['sup'],
|
936
|
+
/* firefox doesn't behave the way we want it to, so we CAN'T use queryCommandState for superscript
|
937
|
+
https://github.com/guardian/scribe/blob/master/BROWSERINCONSISTENCIES.md#documentquerycommandstate */
|
938
|
+
// useQueryState: true
|
939
|
+
contentDefault: '<b>x<sup>1</sup></b>',
|
940
|
+
contentFA: '<i class="fa fa-superscript"></i>'
|
941
|
+
},
|
942
|
+
'subscript': {
|
943
|
+
name: 'subscript',
|
944
|
+
action: 'subscript',
|
945
|
+
aria: 'subscript',
|
946
|
+
tagNames: ['sub'],
|
947
|
+
/* firefox doesn't behave the way we want it to, so we CAN'T use queryCommandState for subscript
|
948
|
+
https://github.com/guardian/scribe/blob/master/BROWSERINCONSISTENCIES.md#documentquerycommandstate */
|
949
|
+
// useQueryState: true
|
950
|
+
contentDefault: '<b>x<sub>1</sub></b>',
|
951
|
+
contentFA: '<i class="fa fa-subscript"></i>'
|
952
|
+
},
|
953
|
+
'image': {
|
954
|
+
name: 'image',
|
955
|
+
action: 'image',
|
956
|
+
aria: 'image',
|
957
|
+
tagNames: ['img'],
|
958
|
+
contentDefault: '<b>image</b>',
|
959
|
+
contentFA: '<i class="fa fa-picture-o"></i>'
|
960
|
+
},
|
961
|
+
'quote': {
|
962
|
+
name: 'quote',
|
963
|
+
action: 'append-blockquote',
|
964
|
+
aria: 'blockquote',
|
965
|
+
tagNames: ['blockquote'],
|
966
|
+
contentDefault: '<b>“</b>',
|
967
|
+
contentFA: '<i class="fa fa-quote-right"></i>'
|
968
|
+
},
|
969
|
+
'orderedlist': {
|
970
|
+
name: 'orderedlist',
|
971
|
+
action: 'insertorderedlist',
|
972
|
+
aria: 'ordered list',
|
973
|
+
tagNames: ['ol'],
|
974
|
+
useQueryState: true,
|
975
|
+
contentDefault: '<b>1.</b>',
|
976
|
+
contentFA: '<i class="fa fa-list-ol"></i>'
|
977
|
+
},
|
978
|
+
'unorderedlist': {
|
979
|
+
name: 'unorderedlist',
|
980
|
+
action: 'insertunorderedlist',
|
981
|
+
aria: 'unordered list',
|
982
|
+
tagNames: ['ul'],
|
983
|
+
useQueryState: true,
|
984
|
+
contentDefault: '<b>•</b>',
|
985
|
+
contentFA: '<i class="fa fa-list-ul"></i>'
|
986
|
+
},
|
987
|
+
'pre': {
|
988
|
+
name: 'pre',
|
989
|
+
action: 'append-pre',
|
990
|
+
aria: 'preformatted text',
|
991
|
+
tagNames: ['pre'],
|
992
|
+
contentDefault: '<b>0101</b>',
|
993
|
+
contentFA: '<i class="fa fa-code fa-lg"></i>'
|
994
|
+
},
|
995
|
+
'indent': {
|
996
|
+
name: 'indent',
|
997
|
+
action: 'indent',
|
998
|
+
aria: 'indent',
|
999
|
+
tagNames: [],
|
1000
|
+
contentDefault: '<b>→</b>',
|
1001
|
+
contentFA: '<i class="fa fa-indent"></i>'
|
1002
|
+
},
|
1003
|
+
'outdent': {
|
1004
|
+
name: 'outdent',
|
1005
|
+
action: 'outdent',
|
1006
|
+
aria: 'outdent',
|
1007
|
+
tagNames: [],
|
1008
|
+
contentDefault: '<b>←</b>',
|
1009
|
+
contentFA: '<i class="fa fa-outdent"></i>'
|
1010
|
+
},
|
1011
|
+
'justifyCenter': {
|
1012
|
+
name: 'justifyCenter',
|
1013
|
+
action: 'justifyCenter',
|
1014
|
+
aria: 'center justify',
|
1015
|
+
tagNames: [],
|
1016
|
+
style: {
|
1017
|
+
prop: 'text-align',
|
1018
|
+
value: 'center'
|
1019
|
+
},
|
1020
|
+
contentDefault: '<b>C</b>',
|
1021
|
+
contentFA: '<i class="fa fa-align-center"></i>'
|
1022
|
+
},
|
1023
|
+
'justifyFull': {
|
1024
|
+
name: 'justifyFull',
|
1025
|
+
action: 'justifyFull',
|
1026
|
+
aria: 'full justify',
|
1027
|
+
tagNames: [],
|
1028
|
+
style: {
|
1029
|
+
prop: 'text-align',
|
1030
|
+
value: 'justify'
|
1031
|
+
},
|
1032
|
+
contentDefault: '<b>J</b>',
|
1033
|
+
contentFA: '<i class="fa fa-align-justify"></i>'
|
1034
|
+
},
|
1035
|
+
'justifyLeft': {
|
1036
|
+
name: 'justifyLeft',
|
1037
|
+
action: 'justifyLeft',
|
1038
|
+
aria: 'left justify',
|
1039
|
+
tagNames: [],
|
1040
|
+
style: {
|
1041
|
+
prop: 'text-align',
|
1042
|
+
value: 'left'
|
1043
|
+
},
|
1044
|
+
contentDefault: '<b>L</b>',
|
1045
|
+
contentFA: '<i class="fa fa-align-left"></i>'
|
1046
|
+
},
|
1047
|
+
'justifyRight': {
|
1048
|
+
name: 'justifyRight',
|
1049
|
+
action: 'justifyRight',
|
1050
|
+
aria: 'right justify',
|
1051
|
+
tagNames: [],
|
1052
|
+
style: {
|
1053
|
+
prop: 'text-align',
|
1054
|
+
value: 'right'
|
1055
|
+
},
|
1056
|
+
contentDefault: '<b>R</b>',
|
1057
|
+
contentFA: '<i class="fa fa-align-right"></i>'
|
1058
|
+
},
|
1059
|
+
'header1': {
|
1060
|
+
name: 'header1',
|
1061
|
+
action: function (options) {
|
1062
|
+
return 'append-' + options.firstHeader;
|
1063
|
+
},
|
1064
|
+
aria: function (options) {
|
1065
|
+
return options.firstHeader;
|
1066
|
+
},
|
1067
|
+
tagNames: function (options) {
|
1068
|
+
return [options.firstHeader];
|
1069
|
+
},
|
1070
|
+
contentDefault: '<b>H1</b>'
|
1071
|
+
},
|
1072
|
+
'header2': {
|
1073
|
+
name: 'header2',
|
1074
|
+
action: function (options) {
|
1075
|
+
return 'append-' + options.secondHeader;
|
1076
|
+
},
|
1077
|
+
aria: function (options) {
|
1078
|
+
return options.secondHeader;
|
1079
|
+
},
|
1080
|
+
tagNames: function (options) {
|
1081
|
+
return [options.secondHeader];
|
1082
|
+
},
|
1083
|
+
contentDefault: '<b>H2</b>'
|
1084
|
+
},
|
1085
|
+
// Known inline elements that are not removed, or not removed consistantly across browsers:
|
1086
|
+
// <span>, <label>, <br>
|
1087
|
+
'removeFormat': {
|
1088
|
+
name: 'removeFormat',
|
1089
|
+
aria: 'remove formatting',
|
1090
|
+
action: 'removeFormat',
|
1091
|
+
contentDefault: '<b>X</b>',
|
1092
|
+
contentFA: '<i class="fa fa-eraser"></i>'
|
1093
|
+
}
|
1094
|
+
};
|
1095
|
+
|
1096
|
+
})();
|
1097
|
+
var editorDefaults;
|
1098
|
+
(function(){
|
1099
|
+
|
1100
|
+
// summary: The default options hash used by the Editor
|
1101
|
+
|
1102
|
+
editorDefaults = {
|
1103
|
+
|
1104
|
+
allowMultiParagraphSelection: true,
|
1105
|
+
anchorInputPlaceholder: 'Paste or type a link',
|
1106
|
+
anchorInputCheckboxLabel: 'Open in new window',
|
1107
|
+
anchorPreviewHideDelay: 500,
|
1108
|
+
buttons: ['bold', 'italic', 'underline', 'anchor', 'header1', 'header2', 'quote'],
|
1109
|
+
buttonLabels: false,
|
1110
|
+
checkLinkFormat: false,
|
1111
|
+
delay: 0,
|
1112
|
+
diffLeft: 0,
|
1113
|
+
diffTop: -10,
|
1114
|
+
disableReturn: false,
|
1115
|
+
disableDoubleReturn: false,
|
1116
|
+
disableToolbar: false,
|
1117
|
+
disableAnchorPreview: false,
|
1118
|
+
disableEditing: false,
|
1119
|
+
disablePlaceholders: false,
|
1120
|
+
toolbarAlign: 'center',
|
1121
|
+
elementsContainer: false,
|
1122
|
+
imageDragging: true,
|
1123
|
+
standardizeSelectionStart: false,
|
1124
|
+
contentWindow: window,
|
1125
|
+
ownerDocument: document,
|
1126
|
+
firstHeader: 'h3',
|
1127
|
+
placeholder: 'Type your text',
|
1128
|
+
secondHeader: 'h4',
|
1129
|
+
targetBlank: false,
|
1130
|
+
anchorTarget: false,
|
1131
|
+
anchorButton: false,
|
1132
|
+
anchorButtonClass: 'btn',
|
1133
|
+
extensions: {},
|
1134
|
+
activeButtonClass: 'medium-editor-button-active',
|
1135
|
+
firstButtonClass: 'medium-editor-button-first',
|
1136
|
+
lastButtonClass: 'medium-editor-button-last',
|
1137
|
+
spellcheck: true,
|
1138
|
+
|
1139
|
+
paste: {
|
1140
|
+
forcePlainText: true,
|
1141
|
+
cleanPastedHTML: false,
|
1142
|
+
cleanAttrs: ['class', 'style', 'dir'],
|
1143
|
+
cleanTags: ['meta']
|
1144
|
+
}
|
1145
|
+
|
1146
|
+
};
|
1147
|
+
|
1148
|
+
})();
|
1149
|
+
|
1150
|
+
var Extension;
|
1151
|
+
(function(){
|
1152
|
+
|
1153
|
+
/* global Util */
|
1154
|
+
|
1155
|
+
Extension = function (options) {
|
1156
|
+
Util.extend(this, options);
|
1157
|
+
};
|
1158
|
+
|
1159
|
+
Extension.extend = function (protoProps) {
|
1160
|
+
// magic extender thinger. mostly borrowed from backbone/goog.inherits
|
1161
|
+
// place this function on some thing you want extend-able.
|
1162
|
+
//
|
1163
|
+
// example:
|
1164
|
+
//
|
1165
|
+
// function Thing(args){
|
1166
|
+
// this.options = args;
|
1167
|
+
// }
|
1168
|
+
//
|
1169
|
+
// Thing.prototype = { foo: "bar" };
|
1170
|
+
// Thing.extend = extenderify;
|
1171
|
+
//
|
1172
|
+
// var ThingTwo = Thing.extend({ foo: "baz" });
|
1173
|
+
//
|
1174
|
+
// var thingOne = new Thing(); // foo === bar
|
1175
|
+
// var thingTwo = new ThingTwo(); // foo == baz
|
1176
|
+
//
|
1177
|
+
// which seems like some simply shallow copy nonsense
|
1178
|
+
// at first, but a lot more is going on there.
|
1179
|
+
//
|
1180
|
+
// passing a `constructor` to the extend props
|
1181
|
+
// will cause the instance to instantiate through that
|
1182
|
+
// instead of the parent's constructor.
|
1183
|
+
|
1184
|
+
var parent = this, child;
|
1185
|
+
|
1186
|
+
// The constructor function for the new subclass is either defined by you
|
1187
|
+
// (the "constructor" property in your `extend` definition), or defaulted
|
1188
|
+
// by us to simply call the parent's constructor.
|
1189
|
+
|
1190
|
+
if (protoProps && protoProps.hasOwnProperty("constructor")) {
|
1191
|
+
child = protoProps.constructor;
|
1192
|
+
} else {
|
1193
|
+
child = function () { return parent.apply(this, arguments); };
|
1194
|
+
}
|
1195
|
+
|
1196
|
+
// das statics (.extend comes over, so your subclass can have subclasses too)
|
1197
|
+
Util.extend(child, parent);
|
1198
|
+
|
1199
|
+
// Set the prototype chain to inherit from `parent`, without calling
|
1200
|
+
// `parent`'s constructor function.
|
1201
|
+
var Surrogate = function(){ this.constructor = child; };
|
1202
|
+
Surrogate.prototype = parent.prototype;
|
1203
|
+
child.prototype = new Surrogate();
|
1204
|
+
|
1205
|
+
if (protoProps) { Util.extend(child.prototype, protoProps); }
|
1206
|
+
|
1207
|
+
// todo: $super?
|
1208
|
+
|
1209
|
+
return child;
|
1210
|
+
};
|
1211
|
+
|
1212
|
+
Extension.prototype = {
|
1213
|
+
init: function(/* instance */){
|
1214
|
+
// called when properly decorated and used.
|
1215
|
+
// has a .base value pointing to the editor
|
1216
|
+
// owning us. has been given a .name if no
|
1217
|
+
// name present
|
1218
|
+
},
|
1219
|
+
|
1220
|
+
/* parent: [boolean]
|
1221
|
+
*
|
1222
|
+
* setting this to true will set the .base property
|
1223
|
+
* of the extension to be a reference to the
|
1224
|
+
* medium-editor instance that is using the extension
|
1225
|
+
*/
|
1226
|
+
parent: false,
|
1227
|
+
|
1228
|
+
/* base: [MediumEditor instance]
|
1229
|
+
*
|
1230
|
+
* If .parent is set to true, this will be set to the
|
1231
|
+
* current MediumEditor instance before init() is called
|
1232
|
+
*/
|
1233
|
+
base: null,
|
1234
|
+
|
1235
|
+
/* name: [string]
|
1236
|
+
*
|
1237
|
+
* 'name' of the extension, used for retrieving the extension.
|
1238
|
+
* If not set, MediumEditor will set this to be the key
|
1239
|
+
* used when passing the extension into MediumEditor via the
|
1240
|
+
* 'extensions' option
|
1241
|
+
*/
|
1242
|
+
name: null,
|
1243
|
+
|
1244
|
+
/* checkState: [function (node)]
|
1245
|
+
*
|
1246
|
+
* If implemented, this function will be called one or more times
|
1247
|
+
* the state of the editor & toolbar are updated.
|
1248
|
+
* When the state is updated, the editor does the following:
|
1249
|
+
*
|
1250
|
+
* 1) Find the parent node containing the current selection
|
1251
|
+
* 2) Call checkState on the extension, passing the node as an argument
|
1252
|
+
* 3) Get tha parent node of the previous node
|
1253
|
+
* 4) Repeat steps #2 and #3 until we move outside the parent contenteditable
|
1254
|
+
*/
|
1255
|
+
checkState: null,
|
1256
|
+
|
1257
|
+
/* getButton: [function ()]
|
1258
|
+
*
|
1259
|
+
* If implemented, this function will be called when
|
1260
|
+
* the toolbar is being created. The DOM Element returned
|
1261
|
+
* by this function will be appended to the toolbar along
|
1262
|
+
* with any other buttons.
|
1263
|
+
*/
|
1264
|
+
getButton: null,
|
1265
|
+
|
1266
|
+
/* As alternatives to checkState, these functions provide a more structured
|
1267
|
+
* path to updating the state of an extension (usually a button) whenever
|
1268
|
+
* the state of the editor & toolbar are updated.
|
1269
|
+
*/
|
1270
|
+
|
1271
|
+
/* queryCommandState: [function ()]
|
1272
|
+
*
|
1273
|
+
* If implemented, this function will be called once on each extension
|
1274
|
+
* when the state of the editor/toolbar is being updated.
|
1275
|
+
*
|
1276
|
+
* If this function returns a non-null value, the exntesion will
|
1277
|
+
* be ignored as the code climbs the dom tree.
|
1278
|
+
*
|
1279
|
+
* If this function returns true, and the setActive() function is defined
|
1280
|
+
* setActive() will be called
|
1281
|
+
*/
|
1282
|
+
queryCommandState: null,
|
1283
|
+
|
1284
|
+
/* isActive: [function ()]
|
1285
|
+
*
|
1286
|
+
* If implemented, this function will be called when MediumEditor
|
1287
|
+
* has determined that this extension is 'active' for the current selection.
|
1288
|
+
* This may be called when the editor & toolbar are being updated,
|
1289
|
+
* but only if queryCommandState() or isAlreadyApplied() functions
|
1290
|
+
* are implemented, and when called, return true.
|
1291
|
+
*/
|
1292
|
+
isActive: null,
|
1293
|
+
|
1294
|
+
/* isAlreadyApplied: [function (node)]
|
1295
|
+
*
|
1296
|
+
* If implemented, this function is similar to checkState() in
|
1297
|
+
* that it will be calle repeatedly as MediumEditor moves up
|
1298
|
+
* the DOM to update the editor & toolbar after a state change.
|
1299
|
+
*
|
1300
|
+
* NOTE: This function will NOT be called if checkState() has
|
1301
|
+
* been implemented. This function will NOT be called if
|
1302
|
+
* queryCommandState() is implemented and returns a non-null
|
1303
|
+
* value when called
|
1304
|
+
*/
|
1305
|
+
isAlreadyApplied: null,
|
1306
|
+
|
1307
|
+
/* setActive: [function ()]
|
1308
|
+
*
|
1309
|
+
* If implemented, this function is called when MediumEditor knows
|
1310
|
+
* that this extension is currently enabled. Currently, this
|
1311
|
+
* function is called when updating the editor & toolbar, and
|
1312
|
+
* only if queryCommandState() or isAlreadyApplied(node) return
|
1313
|
+
* true when called
|
1314
|
+
*/
|
1315
|
+
setActive: null,
|
1316
|
+
|
1317
|
+
/* setInactive: [function ()]
|
1318
|
+
*
|
1319
|
+
* If implemented, this function is called when MediumEditor knows
|
1320
|
+
* that this extension is currently disabled. Curently, this
|
1321
|
+
* is called at the beginning of each state change for
|
1322
|
+
* the editor & toolbar. After calling this, MediumEditor
|
1323
|
+
* will attempt to update the extension, either via checkState()
|
1324
|
+
* or the combination of queryCommandState(), isAlreadyApplied(node),
|
1325
|
+
* isActive(), and setActive()
|
1326
|
+
*/
|
1327
|
+
setInactive: null,
|
1328
|
+
|
1329
|
+
|
1330
|
+
/* onHide: [function ()]
|
1331
|
+
*
|
1332
|
+
* If implemented, this function is called each time the
|
1333
|
+
* toolbar is hidden
|
1334
|
+
*/
|
1335
|
+
onHide: null
|
1336
|
+
};
|
1337
|
+
|
1338
|
+
})();
|
1339
|
+
var Selection;
|
1340
|
+
|
1341
|
+
(function () {
|
1342
|
+
'use strict';
|
1343
|
+
|
1344
|
+
Selection = {
|
1345
|
+
findMatchingSelectionParent: function (testElementFunction, contentWindow) {
|
1346
|
+
var selection = contentWindow.getSelection(), range, current;
|
1347
|
+
|
1348
|
+
if (selection.rangeCount === 0) {
|
1349
|
+
return false;
|
1350
|
+
}
|
1351
|
+
|
1352
|
+
range = selection.getRangeAt(0);
|
1353
|
+
current = range.commonAncestorContainer;
|
1354
|
+
|
1355
|
+
return Util.traverseUp(current, testElementFunction);
|
1356
|
+
},
|
1357
|
+
|
1358
|
+
getSelectionElement: function (contentWindow) {
|
1359
|
+
return this.findMatchingSelectionParent(function (el) {
|
1360
|
+
return el.getAttribute('data-medium-element');
|
1361
|
+
}, contentWindow);
|
1362
|
+
},
|
1363
|
+
|
1364
|
+
selectionInContentEditableFalse: function (contentWindow) {
|
1365
|
+
return this.findMatchingSelectionParent(function (el) {
|
1366
|
+
return (el && el.nodeName !== '#text' && el.getAttribute('contenteditable') === 'false');
|
1367
|
+
}, contentWindow);
|
1368
|
+
},
|
1369
|
+
|
1370
|
+
// http://stackoverflow.com/questions/4176923/html-of-selected-text
|
1371
|
+
// by Tim Down
|
1372
|
+
getSelectionHtml: function getSelectionHtml() {
|
1373
|
+
var i,
|
1374
|
+
html = '',
|
1375
|
+
sel = this.options.contentWindow.getSelection(),
|
1376
|
+
len,
|
1377
|
+
container;
|
1378
|
+
if (sel.rangeCount) {
|
1379
|
+
container = this.options.ownerDocument.createElement('div');
|
1380
|
+
for (i = 0, len = sel.rangeCount; i < len; i += 1) {
|
1381
|
+
container.appendChild(sel.getRangeAt(i).cloneContents());
|
1382
|
+
}
|
1383
|
+
html = container.innerHTML;
|
1384
|
+
}
|
1385
|
+
return html;
|
1386
|
+
},
|
1387
|
+
|
1388
|
+
/**
|
1389
|
+
* Find the caret position within an element irrespective of any inline tags it may contain.
|
1390
|
+
*
|
1391
|
+
* @param {DOMElement} An element containing the cursor to find offsets relative to.
|
1392
|
+
* @param {Range} A Range representing cursor position. Will window.getSelection if none is passed.
|
1393
|
+
* @return {Object} 'left' and 'right' attributes contain offsets from begining and end of Element
|
1394
|
+
*/
|
1395
|
+
getCaretOffsets: function getCaretOffsets(element, range) {
|
1396
|
+
var preCaretRange, postCaretRange;
|
1397
|
+
|
1398
|
+
if (!range) {
|
1399
|
+
range = window.getSelection().getRangeAt(0);
|
1400
|
+
}
|
1401
|
+
|
1402
|
+
preCaretRange = range.cloneRange();
|
1403
|
+
postCaretRange = range.cloneRange();
|
1404
|
+
|
1405
|
+
preCaretRange.selectNodeContents(element);
|
1406
|
+
preCaretRange.setEnd(range.endContainer, range.endOffset);
|
1407
|
+
|
1408
|
+
postCaretRange.selectNodeContents(element);
|
1409
|
+
postCaretRange.setStart(range.endContainer, range.endOffset);
|
1410
|
+
|
1411
|
+
return {
|
1412
|
+
left: preCaretRange.toString().length,
|
1413
|
+
right: postCaretRange.toString().length
|
1414
|
+
};
|
1415
|
+
},
|
1416
|
+
|
1417
|
+
// http://stackoverflow.com/questions/15867542/range-object-get-selection-parent-node-chrome-vs-firefox
|
1418
|
+
rangeSelectsSingleNode: function (range) {
|
1419
|
+
var startNode = range.startContainer;
|
1420
|
+
return startNode === range.endContainer &&
|
1421
|
+
startNode.hasChildNodes() &&
|
1422
|
+
range.endOffset === range.startOffset + 1;
|
1423
|
+
},
|
1424
|
+
|
1425
|
+
getSelectedParentElement: function (range) {
|
1426
|
+
var selectedParentElement = null;
|
1427
|
+
if (this.rangeSelectsSingleNode(range) && range.startContainer.childNodes[range.startOffset].nodeType !== 3) {
|
1428
|
+
selectedParentElement = range.startContainer.childNodes[range.startOffset];
|
1429
|
+
} else if (range.startContainer.nodeType === 3) {
|
1430
|
+
selectedParentElement = range.startContainer.parentNode;
|
1431
|
+
} else {
|
1432
|
+
selectedParentElement = range.startContainer;
|
1433
|
+
}
|
1434
|
+
return selectedParentElement;
|
1435
|
+
},
|
1436
|
+
|
1437
|
+
getSelectedElements: function (doc) {
|
1438
|
+
var selection = doc.getSelection(),
|
1439
|
+
range,
|
1440
|
+
toRet,
|
1441
|
+
currNode;
|
1442
|
+
|
1443
|
+
if (!selection.rangeCount ||
|
1444
|
+
!selection.getRangeAt(0).commonAncestorContainer) {
|
1445
|
+
return [];
|
1446
|
+
}
|
1447
|
+
|
1448
|
+
range = selection.getRangeAt(0);
|
1449
|
+
|
1450
|
+
if (range.commonAncestorContainer.nodeType === 3) {
|
1451
|
+
toRet = [];
|
1452
|
+
currNode = range.commonAncestorContainer;
|
1453
|
+
while (currNode.parentNode && currNode.parentNode.childNodes.length === 1) {
|
1454
|
+
toRet.push(currNode.parentNode);
|
1455
|
+
currNode = currNode.parentNode;
|
1456
|
+
}
|
1457
|
+
|
1458
|
+
return toRet;
|
1459
|
+
}
|
1460
|
+
|
1461
|
+
return [].filter.call(range.commonAncestorContainer.getElementsByTagName('*'), function (el) {
|
1462
|
+
return (typeof selection.containsNode === 'function') ? selection.containsNode(el, true) : true;
|
1463
|
+
});
|
1464
|
+
},
|
1465
|
+
|
1466
|
+
selectNode: function (node, doc) {
|
1467
|
+
var range = doc.createRange(),
|
1468
|
+
sel = doc.getSelection();
|
1469
|
+
|
1470
|
+
range.selectNodeContents(node);
|
1471
|
+
sel.removeAllRanges();
|
1472
|
+
sel.addRange(range);
|
1473
|
+
}
|
1474
|
+
};
|
1475
|
+
}());
|
1476
|
+
|
1477
|
+
var Events;
|
1478
|
+
|
1479
|
+
(function () {
|
1480
|
+
'use strict';
|
1481
|
+
|
1482
|
+
Events = function (instance) {
|
1483
|
+
this.base = instance;
|
1484
|
+
this.options = this.base.options;
|
1485
|
+
this.events = [];
|
1486
|
+
this.customEvents = {};
|
1487
|
+
this.listeners = {};
|
1488
|
+
};
|
1489
|
+
|
1490
|
+
Events.prototype = {
|
1491
|
+
|
1492
|
+
// Helpers for event handling
|
1493
|
+
|
1494
|
+
attachDOMEvent: function (target, event, listener, useCapture) {
|
1495
|
+
target.addEventListener(event, listener, useCapture);
|
1496
|
+
this.events.push([target, event, listener, useCapture]);
|
1497
|
+
},
|
1498
|
+
|
1499
|
+
detachDOMEvent: function (target, event, listener, useCapture) {
|
1500
|
+
var index = this.indexOfListener(target, event, listener, useCapture),
|
1501
|
+
e;
|
1502
|
+
if (index !== -1) {
|
1503
|
+
e = this.events.splice(index, 1)[0];
|
1504
|
+
e[0].removeEventListener(e[1], e[2], e[3]);
|
1505
|
+
}
|
1506
|
+
},
|
1507
|
+
|
1508
|
+
indexOfListener: function (target, event, listener, useCapture) {
|
1509
|
+
var i, n, item;
|
1510
|
+
for (i = 0, n = this.events.length; i < n; i = i + 1) {
|
1511
|
+
item = this.events[i];
|
1512
|
+
if (item[0] === target && item[1] === event && item[2] === listener && item[3] === useCapture) {
|
1513
|
+
return i;
|
1514
|
+
}
|
1515
|
+
}
|
1516
|
+
return -1;
|
1517
|
+
},
|
1518
|
+
|
1519
|
+
detachAllDOMEvents: function () {
|
1520
|
+
var e = this.events.pop();
|
1521
|
+
while (e) {
|
1522
|
+
e[0].removeEventListener(e[1], e[2], e[3]);
|
1523
|
+
e = this.events.pop();
|
1524
|
+
}
|
1525
|
+
},
|
1526
|
+
|
1527
|
+
// custom events
|
1528
|
+
attachCustomEvent: function (event, listener) {
|
1529
|
+
this.setupListener(event);
|
1530
|
+
// If we don't suppot this custom event, don't do anything
|
1531
|
+
if (this.listeners[event]) {
|
1532
|
+
if (!this.customEvents[event]) {
|
1533
|
+
this.customEvents[event] = [];
|
1534
|
+
}
|
1535
|
+
this.customEvents[event].push(listener);
|
1536
|
+
}
|
1537
|
+
},
|
1538
|
+
|
1539
|
+
detachCustomEvent: function (event, listener) {
|
1540
|
+
var index = this.indexOfCustomListener(event, listener);
|
1541
|
+
if (index !== -1) {
|
1542
|
+
this.customEvents[event].splice(index, 1);
|
1543
|
+
// TODO: If array is empty, should detach internal listeners via destoryListener()
|
1544
|
+
}
|
1545
|
+
},
|
1546
|
+
|
1547
|
+
indexOfCustomListener: function (event, listener) {
|
1548
|
+
if (!this.customEvents[event] || !this.customEvents[event].length) {
|
1549
|
+
return -1;
|
1550
|
+
}
|
1551
|
+
|
1552
|
+
return this.customEvents[event].indexOf(listener);
|
1553
|
+
},
|
1554
|
+
|
1555
|
+
detachAllCustomEvents: function () {
|
1556
|
+
this.customEvents = {};
|
1557
|
+
// TODO: Should detach internal listeners here via destroyListener()
|
1558
|
+
},
|
1559
|
+
|
1560
|
+
triggerCustomEvent: function (name, data, editable) {
|
1561
|
+
if (this.customEvents[name]) {
|
1562
|
+
this.customEvents[name].forEach(function (listener) {
|
1563
|
+
listener(data, editable);
|
1564
|
+
});
|
1565
|
+
}
|
1566
|
+
},
|
1567
|
+
|
1568
|
+
// Listening to browser events to emit events medium-editor cares about
|
1569
|
+
|
1570
|
+
setupListener: function (name) {
|
1571
|
+
if (this.listeners[name]) {
|
1572
|
+
return;
|
1573
|
+
}
|
1574
|
+
|
1575
|
+
switch (name) {
|
1576
|
+
case 'externalInteraction':
|
1577
|
+
// Detecting when user has interacted with elements outside of MediumEditor
|
1578
|
+
this.attachDOMEvent(this.options.ownerDocument.body, 'mousedown', this.handleBodyMousedown.bind(this), true);
|
1579
|
+
this.attachDOMEvent(this.options.ownerDocument.body, 'click', this.handleBodyClick.bind(this), true);
|
1580
|
+
this.attachDOMEvent(this.options.ownerDocument.body, 'focus', this.handleBodyFocus.bind(this), true);
|
1581
|
+
this.listeners[name] = true;
|
1582
|
+
break;
|
1583
|
+
case 'blur':
|
1584
|
+
// Detecting when focus is lost
|
1585
|
+
this.setupListener('externalInteraction');
|
1586
|
+
this.listeners[name] = true;
|
1587
|
+
break;
|
1588
|
+
case 'focus':
|
1589
|
+
// Detecting when focus moves into some part of MediumEditor
|
1590
|
+
this.setupListener('externalInteraction');
|
1591
|
+
this.listeners[name] = true;
|
1592
|
+
break;
|
1593
|
+
case 'editableClick':
|
1594
|
+
// Detecting click in the contenteditables
|
1595
|
+
this.base.elements.forEach(function (element) {
|
1596
|
+
this.attachDOMEvent(element, 'click', this.handleClick.bind(this));
|
1597
|
+
}.bind(this));
|
1598
|
+
this.listeners[name] = true;
|
1599
|
+
break;
|
1600
|
+
case 'editableBlur':
|
1601
|
+
// Detecting blur in the contenteditables
|
1602
|
+
this.base.elements.forEach(function (element) {
|
1603
|
+
this.attachDOMEvent(element, 'blur', this.handleBlur.bind(this));
|
1604
|
+
}.bind(this));
|
1605
|
+
this.listeners[name] = true;
|
1606
|
+
break;
|
1607
|
+
case 'editableKeypress':
|
1608
|
+
// Detecting keypress in the contenteditables
|
1609
|
+
this.base.elements.forEach(function (element) {
|
1610
|
+
this.attachDOMEvent(element, 'keypress', this.handleKeypress.bind(this));
|
1611
|
+
}.bind(this));
|
1612
|
+
this.listeners[name] = true;
|
1613
|
+
break;
|
1614
|
+
case 'editableKeyup':
|
1615
|
+
// Detecting keyup in the contenteditables
|
1616
|
+
this.base.elements.forEach(function (element) {
|
1617
|
+
this.attachDOMEvent(element, 'keyup', this.handleKeyup.bind(this));
|
1618
|
+
}.bind(this));
|
1619
|
+
this.listeners[name] = true;
|
1620
|
+
break;
|
1621
|
+
case 'editableKeydown':
|
1622
|
+
// Detecting keydown on the contenteditables
|
1623
|
+
this.base.elements.forEach(function (element) {
|
1624
|
+
this.attachDOMEvent(element, 'keydown', this.handleKeydown.bind(this));
|
1625
|
+
}.bind(this));
|
1626
|
+
this.listeners[name] = true;
|
1627
|
+
break;
|
1628
|
+
case 'editableKeydownEnter':
|
1629
|
+
// Detecting keydown for ENTER on the contenteditables
|
1630
|
+
this.setupListener('editableKeydown');
|
1631
|
+
this.listeners[name] = true;
|
1632
|
+
break;
|
1633
|
+
case 'editableKeydownTab':
|
1634
|
+
// Detecting keydown for TAB on the contenteditable
|
1635
|
+
this.setupListener('editableKeydown');
|
1636
|
+
this.listeners[name] = true;
|
1637
|
+
break;
|
1638
|
+
case 'editableKeydownDelete':
|
1639
|
+
// Detecting keydown for DELETE/BACKSPACE on the contenteditables
|
1640
|
+
this.setupListener('editableKeydown');
|
1641
|
+
this.listeners[name] = true;
|
1642
|
+
break;
|
1643
|
+
case 'editableMouseover':
|
1644
|
+
// Detecting mouseover on the contenteditables
|
1645
|
+
this.base.elements.forEach(function (element) {
|
1646
|
+
this.attachDOMEvent(element, 'mouseover', this.handleMouseover.bind(this));
|
1647
|
+
}, this);
|
1648
|
+
this.listeners[name] = true;
|
1649
|
+
break;
|
1650
|
+
case 'editableDrag':
|
1651
|
+
// Detecting dragover and dragleave on the contenteditables
|
1652
|
+
this.base.elements.forEach(function (element) {
|
1653
|
+
this.attachDOMEvent(element, 'dragover', this.handleDragging.bind(this));
|
1654
|
+
this.attachDOMEvent(element, 'dragleave', this.handleDragging.bind(this));
|
1655
|
+
}, this);
|
1656
|
+
this.listeners[name] = true;
|
1657
|
+
break;
|
1658
|
+
case 'editableDrop':
|
1659
|
+
// Detecting drop on the contenteditables
|
1660
|
+
this.base.elements.forEach(function (element) {
|
1661
|
+
this.attachDOMEvent(element, 'drop', this.handleDrop.bind(this));
|
1662
|
+
}, this);
|
1663
|
+
this.listeners[name] = true;
|
1664
|
+
break;
|
1665
|
+
case 'editablePaste':
|
1666
|
+
// Detecting paste on the contenteditables
|
1667
|
+
this.base.elements.forEach(function (element) {
|
1668
|
+
this.attachDOMEvent(element, 'paste', this.handlePaste.bind(this));
|
1669
|
+
}, this);
|
1670
|
+
this.listeners[name] = true;
|
1671
|
+
break;
|
1672
|
+
}
|
1673
|
+
},
|
1674
|
+
|
1675
|
+
focusElement: function (element) {
|
1676
|
+
element.focus();
|
1677
|
+
this.updateFocus(element, { target: element, type: 'focus' });
|
1678
|
+
},
|
1679
|
+
|
1680
|
+
updateFocus: function (target, eventObj) {
|
1681
|
+
var toolbarEl = this.base.toolbar ? this.base.toolbar.getToolbarElement() : null,
|
1682
|
+
anchorPreview = this.base.getExtensionByName('anchor-preview'),
|
1683
|
+
previewEl = (anchorPreview && anchorPreview.getPreviewElement) ? anchorPreview.getPreviewElement() : null,
|
1684
|
+
hadFocus,
|
1685
|
+
toFocus;
|
1686
|
+
|
1687
|
+
this.base.elements.some(function (element) {
|
1688
|
+
// Find the element that has focus
|
1689
|
+
if (!hadFocus && element.getAttribute('data-medium-focused')) {
|
1690
|
+
hadFocus = element;
|
1691
|
+
}
|
1692
|
+
|
1693
|
+
// bail if we found the element that had focus
|
1694
|
+
return !!hadFocus;
|
1695
|
+
}, this);
|
1696
|
+
|
1697
|
+
// For clicks, we need to know if the mousedown that caused the click happened inside the existing focused element.
|
1698
|
+
// If so, we don't want to focus another element
|
1699
|
+
if (hadFocus &&
|
1700
|
+
eventObj.type === 'click' &&
|
1701
|
+
this.lastMousedownTarget &&
|
1702
|
+
(Util.isDescendant(hadFocus, this.lastMousedownTarget, true) ||
|
1703
|
+
Util.isDescendant(toolbarEl, this.lastMousedownTarget, true) ||
|
1704
|
+
Util.isDescendant(previewEl, this.lastMousedownTarget, true))) {
|
1705
|
+
toFocus = hadFocus;
|
1706
|
+
}
|
1707
|
+
|
1708
|
+
if (!toFocus) {
|
1709
|
+
this.base.elements.some(function (element) {
|
1710
|
+
// If the target is part of an editor element, this is the element getting focus
|
1711
|
+
if (!toFocus && (Util.isDescendant(element, target, true))) {
|
1712
|
+
toFocus = element;
|
1713
|
+
}
|
1714
|
+
|
1715
|
+
// bail if we found an element that's getting focus
|
1716
|
+
return !!toFocus;
|
1717
|
+
}, this);
|
1718
|
+
}
|
1719
|
+
|
1720
|
+
// Check if the target is external (not part of the editor, toolbar, or anchorpreview)
|
1721
|
+
var externalEvent = !Util.isDescendant(hadFocus, target, true) &&
|
1722
|
+
!Util.isDescendant(toolbarEl, target, true) &&
|
1723
|
+
!Util.isDescendant(previewEl, target, true);
|
1724
|
+
|
1725
|
+
if (toFocus !== hadFocus) {
|
1726
|
+
// If element has focus, and focus is going outside of editor
|
1727
|
+
// Don't blur focused element if clicking on editor, toolbar, or anchorpreview
|
1728
|
+
if (hadFocus && externalEvent) {
|
1729
|
+
// Trigger blur on the editable that has lost focus
|
1730
|
+
hadFocus.removeAttribute('data-medium-focused');
|
1731
|
+
this.triggerCustomEvent('blur', eventObj, hadFocus);
|
1732
|
+
}
|
1733
|
+
|
1734
|
+
// If focus is going into an editor element
|
1735
|
+
if (toFocus) {
|
1736
|
+
// Trigger focus on the editable that now has focus
|
1737
|
+
toFocus.setAttribute('data-medium-focused', true);
|
1738
|
+
this.triggerCustomEvent('focus', eventObj, toFocus);
|
1739
|
+
}
|
1740
|
+
}
|
1741
|
+
|
1742
|
+
if (externalEvent) {
|
1743
|
+
this.triggerCustomEvent('externalInteraction', eventObj);
|
1744
|
+
}
|
1745
|
+
},
|
1746
|
+
|
1747
|
+
handleBodyClick: function (event) {
|
1748
|
+
this.updateFocus(event.target, event);
|
1749
|
+
},
|
1750
|
+
|
1751
|
+
handleBodyFocus: function (event) {
|
1752
|
+
this.updateFocus(event.target, event);
|
1753
|
+
},
|
1754
|
+
|
1755
|
+
handleBodyMousedown: function (event) {
|
1756
|
+
this.lastMousedownTarget = event.target;
|
1757
|
+
},
|
1758
|
+
|
1759
|
+
handleClick: function (event) {
|
1760
|
+
this.triggerCustomEvent('editableClick', event, event.currentTarget);
|
1761
|
+
},
|
1762
|
+
|
1763
|
+
handleBlur: function (event) {
|
1764
|
+
this.triggerCustomEvent('editableBlur', event, event.currentTarget);
|
1765
|
+
},
|
1766
|
+
|
1767
|
+
handleKeypress: function (event) {
|
1768
|
+
this.triggerCustomEvent('editableKeypress', event, event.currentTarget);
|
1769
|
+
},
|
1770
|
+
|
1771
|
+
handleKeyup: function (event) {
|
1772
|
+
this.triggerCustomEvent('editableKeyup', event, event.currentTarget);
|
1773
|
+
},
|
1774
|
+
|
1775
|
+
handleMouseover: function (event) {
|
1776
|
+
this.triggerCustomEvent('editableMouseover', event, event.currentTarget);
|
1777
|
+
},
|
1778
|
+
|
1779
|
+
handleDragging: function (event) {
|
1780
|
+
this.triggerCustomEvent('editableDrag', event, event.currentTarget);
|
1781
|
+
},
|
1782
|
+
|
1783
|
+
handleDrop: function (event) {
|
1784
|
+
this.triggerCustomEvent('editableDrop', event, event.currentTarget);
|
1785
|
+
},
|
1786
|
+
|
1787
|
+
handlePaste: function (event) {
|
1788
|
+
this.triggerCustomEvent('editablePaste', event, event.currentTarget);
|
1789
|
+
},
|
1790
|
+
|
1791
|
+
handleKeydown: function (event) {
|
1792
|
+
this.triggerCustomEvent('editableKeydown', event, event.currentTarget);
|
1793
|
+
|
1794
|
+
switch (event.which) {
|
1795
|
+
case Util.keyCode.ENTER:
|
1796
|
+
this.triggerCustomEvent('editableKeydownEnter', event, event.currentTarget);
|
1797
|
+
break;
|
1798
|
+
case Util.keyCode.TAB:
|
1799
|
+
this.triggerCustomEvent('editableKeydownTab', event, event.currentTarget);
|
1800
|
+
break;
|
1801
|
+
case Util.keyCode.DELETE:
|
1802
|
+
case Util.keyCode.BACKSPACE:
|
1803
|
+
this.triggerCustomEvent('editableKeydownDelete', event, event.currentTarget);
|
1804
|
+
break;
|
1805
|
+
}
|
1806
|
+
}
|
1807
|
+
};
|
1808
|
+
|
1809
|
+
}());
|
1810
|
+
|
1811
|
+
var DefaultButton;
|
1812
|
+
|
1813
|
+
(function () {
|
1814
|
+
'use strict';
|
1815
|
+
|
1816
|
+
DefaultButton = function (options, instance) {
|
1817
|
+
this.options = options;
|
1818
|
+
this.name = options.name;
|
1819
|
+
this.init(instance);
|
1820
|
+
};
|
1821
|
+
|
1822
|
+
DefaultButton.prototype = {
|
1823
|
+
init: function (instance) {
|
1824
|
+
this.base = instance;
|
1825
|
+
|
1826
|
+
this.button = this.createButton();
|
1827
|
+
this.base.on(this.button, 'click', this.handleClick.bind(this));
|
1828
|
+
if (this.options.key) {
|
1829
|
+
this.base.subscribe('editableKeydown', this.handleKeydown.bind(this));
|
1830
|
+
}
|
1831
|
+
},
|
1832
|
+
getButton: function () {
|
1833
|
+
return this.button;
|
1834
|
+
},
|
1835
|
+
getAction: function () {
|
1836
|
+
return (typeof this.options.action === 'function') ? this.options.action(this.base.options) : this.options.action;
|
1837
|
+
},
|
1838
|
+
getAria: function () {
|
1839
|
+
return (typeof this.options.aria === 'function') ? this.options.aria(this.base.options) : this.options.aria;
|
1840
|
+
},
|
1841
|
+
getTagNames: function () {
|
1842
|
+
return (typeof this.options.tagNames === 'function') ? this.options.tagNames(this.base.options) : this.options.tagNames;
|
1843
|
+
},
|
1844
|
+
createButton: function () {
|
1845
|
+
var button = this.base.options.ownerDocument.createElement('button'),
|
1846
|
+
content = this.options.contentDefault,
|
1847
|
+
ariaLabel = this.getAria();
|
1848
|
+
button.classList.add('medium-editor-action');
|
1849
|
+
button.classList.add('medium-editor-action-' + this.name);
|
1850
|
+
button.setAttribute('data-action', this.getAction());
|
1851
|
+
if (ariaLabel) {
|
1852
|
+
button.setAttribute('title', ariaLabel);
|
1853
|
+
button.setAttribute('aria-label', ariaLabel);
|
1854
|
+
}
|
1855
|
+
if (this.base.options.buttonLabels) {
|
1856
|
+
if (this.base.options.buttonLabels === 'fontawesome' && this.options.contentFA) {
|
1857
|
+
content = this.options.contentFA;
|
1858
|
+
} else if (typeof this.base.options.buttonLabels === 'object' && this.base.options.buttonLabels[this.name]) {
|
1859
|
+
content = this.base.options.buttonLabels[this.options.name];
|
1860
|
+
}
|
1861
|
+
}
|
1862
|
+
button.innerHTML = content;
|
1863
|
+
return button;
|
1864
|
+
},
|
1865
|
+
handleKeydown: function (evt) {
|
1866
|
+
var key, action;
|
1867
|
+
|
1868
|
+
if (evt.ctrlKey || evt.metaKey) {
|
1869
|
+
key = String.fromCharCode(evt.which || evt.keyCode).toLowerCase();
|
1870
|
+
if (this.options.key === key) {
|
1871
|
+
evt.preventDefault();
|
1872
|
+
evt.stopPropagation();
|
1873
|
+
|
1874
|
+
action = this.getAction();
|
1875
|
+
if (action) {
|
1876
|
+
this.base.execAction(action);
|
1877
|
+
}
|
1878
|
+
}
|
1879
|
+
}
|
1880
|
+
},
|
1881
|
+
handleClick: function (evt) {
|
1882
|
+
evt.preventDefault();
|
1883
|
+
evt.stopPropagation();
|
1884
|
+
|
1885
|
+
var action = this.getAction();
|
1886
|
+
|
1887
|
+
if (action) {
|
1888
|
+
this.base.execAction(action);
|
1889
|
+
}
|
1890
|
+
},
|
1891
|
+
isActive: function () {
|
1892
|
+
return this.button.classList.contains(this.base.options.activeButtonClass);
|
1893
|
+
},
|
1894
|
+
setInactive: function () {
|
1895
|
+
this.button.classList.remove(this.base.options.activeButtonClass);
|
1896
|
+
delete this.knownState;
|
1897
|
+
},
|
1898
|
+
setActive: function () {
|
1899
|
+
this.button.classList.add(this.base.options.activeButtonClass);
|
1900
|
+
delete this.knownState;
|
1901
|
+
},
|
1902
|
+
queryCommandState: function () {
|
1903
|
+
var queryState = null;
|
1904
|
+
if (this.options.useQueryState) {
|
1905
|
+
queryState = this.base.queryCommandState(this.getAction());
|
1906
|
+
}
|
1907
|
+
return queryState;
|
1908
|
+
},
|
1909
|
+
isAlreadyApplied: function (node) {
|
1910
|
+
var isMatch = false,
|
1911
|
+
tagNames = this.getTagNames(),
|
1912
|
+
styleVals,
|
1913
|
+
computedStyle;
|
1914
|
+
|
1915
|
+
if (this.knownState === false || this.knownState === true) {
|
1916
|
+
return this.knownState;
|
1917
|
+
}
|
1918
|
+
|
1919
|
+
if (tagNames && tagNames.length > 0 && node.tagName) {
|
1920
|
+
isMatch = tagNames.indexOf(node.tagName.toLowerCase()) !== -1;
|
1921
|
+
}
|
1922
|
+
|
1923
|
+
if (!isMatch && this.options.style) {
|
1924
|
+
styleVals = this.options.style.value.split('|');
|
1925
|
+
computedStyle = this.base.options.contentWindow.getComputedStyle(node, null).getPropertyValue(this.options.style.prop);
|
1926
|
+
styleVals.forEach(function (val) {
|
1927
|
+
if (!this.knownState) {
|
1928
|
+
isMatch = (computedStyle.indexOf(val) !== -1);
|
1929
|
+
// text-decoration is not inherited by default
|
1930
|
+
// so if the computed style for text-decoration doesn't match
|
1931
|
+
// don't write to knownState so we can fallback to other checks
|
1932
|
+
if (isMatch || this.options.style.prop !== 'text-decoration') {
|
1933
|
+
this.knownState = isMatch;
|
1934
|
+
}
|
1935
|
+
}
|
1936
|
+
}, this);
|
1937
|
+
}
|
1938
|
+
|
1939
|
+
return isMatch;
|
1940
|
+
}
|
1941
|
+
};
|
1942
|
+
}());
|
1943
|
+
|
1944
|
+
var PasteHandler;
|
1945
|
+
|
1946
|
+
(function () {
|
1947
|
+
'use strict';
|
1948
|
+
/*jslint regexp: true*/
|
1949
|
+
/*
|
1950
|
+
jslint does not allow character negation, because the negation
|
1951
|
+
will not match any unicode characters. In the regexes in this
|
1952
|
+
block, negation is used specifically to match the end of an html
|
1953
|
+
tag, and in fact unicode characters *should* be allowed.
|
1954
|
+
*/
|
1955
|
+
function createReplacements() {
|
1956
|
+
return [
|
1957
|
+
|
1958
|
+
// replace two bogus tags that begin pastes from google docs
|
1959
|
+
[new RegExp(/<[^>]*docs-internal-guid[^>]*>/gi), ""],
|
1960
|
+
[new RegExp(/<\/b>(<br[^>]*>)?$/gi), ""],
|
1961
|
+
|
1962
|
+
// un-html spaces and newlines inserted by OS X
|
1963
|
+
[new RegExp(/<span class="Apple-converted-space">\s+<\/span>/g), ' '],
|
1964
|
+
[new RegExp(/<br class="Apple-interchange-newline">/g), '<br>'],
|
1965
|
+
|
1966
|
+
// replace google docs italics+bold with a span to be replaced once the html is inserted
|
1967
|
+
[new RegExp(/<span[^>]*(font-style:italic;font-weight:bold|font-weight:bold;font-style:italic)[^>]*>/gi), '<span class="replace-with italic bold">'],
|
1968
|
+
|
1969
|
+
// replace google docs italics with a span to be replaced once the html is inserted
|
1970
|
+
[new RegExp(/<span[^>]*font-style:italic[^>]*>/gi), '<span class="replace-with italic">'],
|
1971
|
+
|
1972
|
+
//[replace google docs bolds with a span to be replaced once the html is inserted
|
1973
|
+
[new RegExp(/<span[^>]*font-weight:bold[^>]*>/gi), '<span class="replace-with bold">'],
|
1974
|
+
|
1975
|
+
// replace manually entered b/i/a tags with real ones
|
1976
|
+
[new RegExp(/<(\/?)(i|b|a)>/gi), '<$1$2>'],
|
1977
|
+
|
1978
|
+
// replace manually a tags with real ones, converting smart-quotes from google docs
|
1979
|
+
[new RegExp(/<a(?:(?!href).)+href=(?:"|”|“|"|“|”)(((?!"|”|“|"|“|”).)*)(?:"|”|“|"|“|”)(?:(?!>).)*>/gi), '<a href="$1">'],
|
1980
|
+
|
1981
|
+
// Newlines between paragraphs in html have no syntactic value,
|
1982
|
+
// but then have a tendency to accidentally become additional paragraphs down the line
|
1983
|
+
[new RegExp(/<\/p>\n+/gi), '</p>'],
|
1984
|
+
[new RegExp(/\n+<p/gi), '<p'],
|
1985
|
+
|
1986
|
+
// Microsoft Word makes these odd tags, like <o:p></o:p>
|
1987
|
+
[new RegExp(/<\/?o:[a-z]*>/gi), '']
|
1988
|
+
];
|
1989
|
+
}
|
1990
|
+
/*jslint regexp: false*/
|
1991
|
+
|
1992
|
+
PasteHandler = Extension.extend({
|
1993
|
+
|
1994
|
+
/* Paste Options */
|
1995
|
+
|
1996
|
+
/* forcePlainText: [boolean]
|
1997
|
+
* Forces pasting as plain text.
|
1998
|
+
*/
|
1999
|
+
forcePlainText: true,
|
2000
|
+
|
2001
|
+
/* cleanPastedHTML: [boolean]
|
2002
|
+
* cleans pasted content from different sources, like google docs etc.
|
2003
|
+
*/
|
2004
|
+
cleanPastedHTML: false,
|
2005
|
+
|
2006
|
+
/* cleanReplacements: [Array]
|
2007
|
+
* custom pairs (2 element arrays) of RegExp and replacement text to use during paste when
|
2008
|
+
* __forcePlainText__ or __cleanPastedHTML__ are `true` OR when calling `cleanPaste(text)` helper method.
|
2009
|
+
*/
|
2010
|
+
cleanReplacements: [],
|
2011
|
+
|
2012
|
+
/* cleanAttrs:: [Array]
|
2013
|
+
* list of element attributes to remove during paste when __cleanPastedHTML__ is `true` or when
|
2014
|
+
* calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods.
|
2015
|
+
*/
|
2016
|
+
cleanAttrs: ['class', 'style', 'dir'],
|
2017
|
+
|
2018
|
+
/* cleanTags: [Array]
|
2019
|
+
* list of element tag names to remove during paste when __cleanPastedHTML__ is `true` or when
|
2020
|
+
* calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods.
|
2021
|
+
*/
|
2022
|
+
cleanTags: ['meta'],
|
2023
|
+
|
2024
|
+
/* ----- internal options needed from base ----- */
|
2025
|
+
"window": window,
|
2026
|
+
"document": document,
|
2027
|
+
targetBlank: false,
|
2028
|
+
disableReturn: false,
|
2029
|
+
|
2030
|
+
// Need a reference to MediumEditor (this.base)
|
2031
|
+
parent: true,
|
2032
|
+
|
2033
|
+
init: function () {
|
2034
|
+
if (this.forcePlainText || this.cleanPastedHTML) {
|
2035
|
+
this.base.subscribe('editablePaste', this.handlePaste.bind(this));
|
2036
|
+
}
|
2037
|
+
},
|
2038
|
+
|
2039
|
+
handlePaste: function (event, element) {
|
2040
|
+
var paragraphs,
|
2041
|
+
html = '',
|
2042
|
+
p,
|
2043
|
+
dataFormatHTML = 'text/html',
|
2044
|
+
dataFormatPlain = 'text/plain',
|
2045
|
+
pastedHTML,
|
2046
|
+
pastedPlain;
|
2047
|
+
|
2048
|
+
if (this.window.clipboardData && event.clipboardData === undefined) {
|
2049
|
+
event.clipboardData = this.window.clipboardData;
|
2050
|
+
// If window.clipboardData exists, but event.clipboardData doesn't exist,
|
2051
|
+
// we're probably in IE. IE only has two possibilities for clipboard
|
2052
|
+
// data format: 'Text' and 'URL'.
|
2053
|
+
//
|
2054
|
+
// Of the two, we want 'Text':
|
2055
|
+
dataFormatHTML = 'Text';
|
2056
|
+
dataFormatPlain = 'Text';
|
2057
|
+
}
|
2058
|
+
|
2059
|
+
if (event.clipboardData &&
|
2060
|
+
event.clipboardData.getData &&
|
2061
|
+
!event.defaultPrevented) {
|
2062
|
+
event.preventDefault();
|
2063
|
+
|
2064
|
+
pastedHTML = event.clipboardData.getData(dataFormatHTML);
|
2065
|
+
pastedPlain = event.clipboardData.getData(dataFormatPlain);
|
2066
|
+
|
2067
|
+
if (!pastedHTML) {
|
2068
|
+
pastedHTML = pastedPlain;
|
2069
|
+
}
|
2070
|
+
|
2071
|
+
if (this.cleanPastedHTML && pastedHTML) {
|
2072
|
+
return this.cleanPaste(pastedHTML);
|
2073
|
+
}
|
2074
|
+
|
2075
|
+
if (!(this.disableReturn || element.getAttribute('data-disable-return'))) {
|
2076
|
+
paragraphs = pastedPlain.split(/[\r\n]+/g);
|
2077
|
+
// If there are no \r\n in data, don't wrap in <p>
|
2078
|
+
if (paragraphs.length > 1) {
|
2079
|
+
for (p = 0; p < paragraphs.length; p += 1) {
|
2080
|
+
if (paragraphs[p] !== '') {
|
2081
|
+
html += '<p>' + Util.htmlEntities(paragraphs[p]) + '</p>';
|
2082
|
+
}
|
2083
|
+
}
|
2084
|
+
} else {
|
2085
|
+
html = Util.htmlEntities(paragraphs[0]);
|
2086
|
+
}
|
2087
|
+
} else {
|
2088
|
+
html = Util.htmlEntities(pastedPlain);
|
2089
|
+
}
|
2090
|
+
Util.insertHTMLCommand(this.document, html);
|
2091
|
+
}
|
2092
|
+
},
|
2093
|
+
|
2094
|
+
cleanPaste: function (text) {
|
2095
|
+
var i, elList, workEl,
|
2096
|
+
el = Selection.getSelectionElement(this.window),
|
2097
|
+
multiline = /<p|<br|<div/.test(text),
|
2098
|
+
replacements = createReplacements().concat(this.cleanReplacements || []);
|
2099
|
+
|
2100
|
+
for (i = 0; i < replacements.length; i += 1) {
|
2101
|
+
text = text.replace(replacements[i][0], replacements[i][1]);
|
2102
|
+
}
|
2103
|
+
|
2104
|
+
if (multiline) {
|
2105
|
+
// double br's aren't converted to p tags, but we want paragraphs.
|
2106
|
+
elList = text.split('<br><br>');
|
2107
|
+
|
2108
|
+
this.pasteHTML('<p>' + elList.join('</p><p>') + '</p>');
|
2109
|
+
|
2110
|
+
try {
|
2111
|
+
this.document.execCommand('insertText', false, "\n");
|
2112
|
+
} catch (ignore) { }
|
2113
|
+
|
2114
|
+
// block element cleanup
|
2115
|
+
elList = el.querySelectorAll('a,p,div,br');
|
2116
|
+
for (i = 0; i < elList.length; i += 1) {
|
2117
|
+
workEl = elList[i];
|
2118
|
+
|
2119
|
+
// Microsoft Word replaces some spaces with newlines.
|
2120
|
+
// While newlines between block elements are meaningless, newlines within
|
2121
|
+
// elements are sometimes actually spaces.
|
2122
|
+
workEl.innerHTML = workEl.innerHTML.replace(/\n/gi, ' ');
|
2123
|
+
|
2124
|
+
switch (workEl.tagName.toLowerCase()) {
|
2125
|
+
case 'a':
|
2126
|
+
if (this.targetBlank) {
|
2127
|
+
Util.setTargetBlank(workEl);
|
2128
|
+
}
|
2129
|
+
break;
|
2130
|
+
case 'p':
|
2131
|
+
case 'div':
|
2132
|
+
this.filterCommonBlocks(workEl);
|
2133
|
+
break;
|
2134
|
+
case 'br':
|
2135
|
+
this.filterLineBreak(workEl);
|
2136
|
+
break;
|
2137
|
+
}
|
2138
|
+
}
|
2139
|
+
} else {
|
2140
|
+
this.pasteHTML(text);
|
2141
|
+
}
|
2142
|
+
},
|
2143
|
+
|
2144
|
+
pasteHTML: function (html, options) {
|
2145
|
+
options = Util.defaults({}, options, {
|
2146
|
+
cleanAttrs: this.cleanAttrs,
|
2147
|
+
cleanTags: this.cleanTags
|
2148
|
+
});
|
2149
|
+
|
2150
|
+
var elList, workEl, i, fragmentBody, pasteBlock = this.document.createDocumentFragment();
|
2151
|
+
|
2152
|
+
pasteBlock.appendChild(this.document.createElement('body'));
|
2153
|
+
|
2154
|
+
fragmentBody = pasteBlock.querySelector('body');
|
2155
|
+
fragmentBody.innerHTML = html;
|
2156
|
+
|
2157
|
+
this.cleanupSpans(fragmentBody);
|
2158
|
+
|
2159
|
+
elList = fragmentBody.querySelectorAll('*');
|
2160
|
+
|
2161
|
+
for (i = 0; i < elList.length; i += 1) {
|
2162
|
+
workEl = elList[i];
|
2163
|
+
Util.cleanupAttrs(workEl, options.cleanAttrs);
|
2164
|
+
Util.cleanupTags(workEl, options.cleanTags);
|
2165
|
+
}
|
2166
|
+
|
2167
|
+
Util.insertHTMLCommand(this.document, fragmentBody.innerHTML.replace(/ /g, ' '));
|
2168
|
+
},
|
2169
|
+
|
2170
|
+
isCommonBlock: function (el) {
|
2171
|
+
return (el && (el.tagName.toLowerCase() === 'p' || el.tagName.toLowerCase() === 'div'));
|
2172
|
+
},
|
2173
|
+
|
2174
|
+
filterCommonBlocks: function (el) {
|
2175
|
+
if (/^\s*$/.test(el.textContent) && el.parentNode) {
|
2176
|
+
el.parentNode.removeChild(el);
|
2177
|
+
}
|
2178
|
+
},
|
2179
|
+
|
2180
|
+
filterLineBreak: function (el) {
|
2181
|
+
|
2182
|
+
if (this.isCommonBlock(el.previousElementSibling)) {
|
2183
|
+
// remove stray br's following common block elements
|
2184
|
+
this.removeWithParent(el);
|
2185
|
+
} else if (this.isCommonBlock(el.parentNode) && (el.parentNode.firstChild === el || el.parentNode.lastChild === el)) {
|
2186
|
+
// remove br's just inside open or close tags of a div/p
|
2187
|
+
this.removeWithParent(el);
|
2188
|
+
} else if (el.parentNode && el.parentNode.childElementCount === 1 && el.parentNode.textContent === '') {
|
2189
|
+
// and br's that are the only child of elements other than div/p
|
2190
|
+
this.removeWithParent(el);
|
2191
|
+
}
|
2192
|
+
},
|
2193
|
+
|
2194
|
+
// remove an element, including its parent, if it is the only element within its parent
|
2195
|
+
removeWithParent: function (el) {
|
2196
|
+
if (el && el.parentNode) {
|
2197
|
+
if (el.parentNode.parentNode && el.parentNode.childElementCount === 1) {
|
2198
|
+
el.parentNode.parentNode.removeChild(el.parentNode);
|
2199
|
+
} else {
|
2200
|
+
el.parentNode.removeChild(el);
|
2201
|
+
}
|
2202
|
+
}
|
2203
|
+
},
|
2204
|
+
|
2205
|
+
cleanupSpans: function (container_el) {
|
2206
|
+
var i,
|
2207
|
+
el,
|
2208
|
+
new_el,
|
2209
|
+
spans = container_el.querySelectorAll('.replace-with'),
|
2210
|
+
isCEF = function (el) {
|
2211
|
+
return (el && el.nodeName !== '#text' && el.getAttribute('contenteditable') === 'false');
|
2212
|
+
};
|
2213
|
+
|
2214
|
+
for (i = 0; i < spans.length; i += 1) {
|
2215
|
+
el = spans[i];
|
2216
|
+
new_el = this.document.createElement(el.classList.contains('bold') ? 'b' : 'i');
|
2217
|
+
|
2218
|
+
if (el.classList.contains('bold') && el.classList.contains('italic')) {
|
2219
|
+
// add an i tag as well if this has both italics and bold
|
2220
|
+
new_el.innerHTML = '<i>' + el.innerHTML + '</i>';
|
2221
|
+
} else {
|
2222
|
+
new_el.innerHTML = el.innerHTML;
|
2223
|
+
}
|
2224
|
+
el.parentNode.replaceChild(new_el, el);
|
2225
|
+
}
|
2226
|
+
|
2227
|
+
spans = container_el.querySelectorAll('span');
|
2228
|
+
for (i = 0; i < spans.length; i += 1) {
|
2229
|
+
el = spans[i];
|
2230
|
+
|
2231
|
+
// bail if span is in contenteditable = false
|
2232
|
+
if (Util.traverseUp(el, isCEF)) {
|
2233
|
+
return false;
|
2234
|
+
}
|
2235
|
+
|
2236
|
+
// remove empty spans, replace others with their contents
|
2237
|
+
Util.unwrap(el, this.document);
|
2238
|
+
}
|
2239
|
+
}
|
2240
|
+
});
|
2241
|
+
|
2242
|
+
}());
|
2243
|
+
|
2244
|
+
var AnchorExtension;
|
2245
|
+
|
2246
|
+
(function () {
|
2247
|
+
'use strict';
|
2248
|
+
|
2249
|
+
function AnchorDerived() {
|
2250
|
+
this.parent = true;
|
2251
|
+
this.options = {
|
2252
|
+
name: 'anchor',
|
2253
|
+
action: 'createLink',
|
2254
|
+
aria: 'link',
|
2255
|
+
tagNames: ['a'],
|
2256
|
+
contentDefault: '<b>#</b>',
|
2257
|
+
contentFA: '<i class="fa fa-link"></i>'
|
2258
|
+
};
|
2259
|
+
this.name = 'anchor';
|
2260
|
+
this.hasForm = true;
|
2261
|
+
}
|
2262
|
+
|
2263
|
+
AnchorDerived.prototype = {
|
2264
|
+
|
2265
|
+
// Button and Extension handling
|
2266
|
+
|
2267
|
+
// labels for the anchor-edit form buttons
|
2268
|
+
formSaveLabel: '✓',
|
2269
|
+
formCloseLabel: '×',
|
2270
|
+
|
2271
|
+
// Called when the button the toolbar is clicked
|
2272
|
+
// Overrides DefaultButton.handleClick
|
2273
|
+
handleClick: function (evt) {
|
2274
|
+
evt.preventDefault();
|
2275
|
+
evt.stopPropagation();
|
2276
|
+
|
2277
|
+
var selectedParentElement = Selection.getSelectedParentElement(Util.getSelectionRange(this.base.options.ownerDocument));
|
2278
|
+
if (selectedParentElement.tagName &&
|
2279
|
+
selectedParentElement.tagName.toLowerCase() === 'a') {
|
2280
|
+
return this.base.execAction('unlink');
|
2281
|
+
}
|
2282
|
+
|
2283
|
+
if (!this.isDisplayed()) {
|
2284
|
+
this.showForm();
|
2285
|
+
}
|
2286
|
+
|
2287
|
+
return false;
|
2288
|
+
},
|
2289
|
+
|
2290
|
+
// Called by medium-editor to append form to the toolbar
|
2291
|
+
getForm: function () {
|
2292
|
+
if (!this.form) {
|
2293
|
+
this.form = this.createForm();
|
2294
|
+
}
|
2295
|
+
return this.form;
|
2296
|
+
},
|
2297
|
+
|
2298
|
+
getTemplate: function () {
|
2299
|
+
|
2300
|
+
var template = [
|
2301
|
+
'<input type="text" class="medium-editor-toolbar-input" placeholder="', this.base.options.anchorInputPlaceholder, '">'
|
2302
|
+
];
|
2303
|
+
|
2304
|
+
template.push(
|
2305
|
+
'<a href="#" class="medium-editor-toolbar-save">',
|
2306
|
+
this.base.options.buttonLabels === 'fontawesome' ? '<i class="fa fa-check"></i>' : this.formSaveLabel,
|
2307
|
+
'</a>'
|
2308
|
+
);
|
2309
|
+
|
2310
|
+
template.push('<a href="#" class="medium-editor-toolbar-close">',
|
2311
|
+
this.base.options.buttonLabels === 'fontawesome' ? '<i class="fa fa-times"></i>' : this.formCloseLabel,
|
2312
|
+
'</a>');
|
2313
|
+
|
2314
|
+
// both of these options are slightly moot with the ability to
|
2315
|
+
// override the various form buildup/serialize functions.
|
2316
|
+
|
2317
|
+
if (this.base.options.anchorTarget) {
|
2318
|
+
// fixme: ideally, this options.anchorInputCheckboxLabel would be a formLabel too,
|
2319
|
+
// figure out how to deprecate? also consider `fa-` icon default implcations.
|
2320
|
+
template.push(
|
2321
|
+
'<input type="checkbox" class="medium-editor-toolbar-anchor-target">',
|
2322
|
+
'<label>',
|
2323
|
+
this.base.options.anchorInputCheckboxLabel,
|
2324
|
+
'</label>'
|
2325
|
+
);
|
2326
|
+
}
|
2327
|
+
|
2328
|
+
if (this.base.options.anchorButton) {
|
2329
|
+
// fixme: expose this `Button` text as a formLabel property, too
|
2330
|
+
// and provide similar access to a `fa-` icon default.
|
2331
|
+
template.push(
|
2332
|
+
'<input type="checkbox" class="medium-editor-toolbar-anchor-button">',
|
2333
|
+
'<label>Button</label>'
|
2334
|
+
);
|
2335
|
+
}
|
2336
|
+
|
2337
|
+
return template.join("");
|
2338
|
+
|
2339
|
+
},
|
2340
|
+
|
2341
|
+
// Used by medium-editor when the default toolbar is to be displayed
|
2342
|
+
isDisplayed: function () {
|
2343
|
+
return this.getForm().style.display === 'block';
|
2344
|
+
},
|
2345
|
+
|
2346
|
+
hideForm: function () {
|
2347
|
+
this.getForm().style.display = 'none';
|
2348
|
+
this.getInput().value = '';
|
2349
|
+
},
|
2350
|
+
|
2351
|
+
showForm: function (link_value) {
|
2352
|
+
var input = this.getInput();
|
2353
|
+
|
2354
|
+
this.base.saveSelection();
|
2355
|
+
this.base.hideToolbarDefaultActions();
|
2356
|
+
this.getForm().style.display = 'block';
|
2357
|
+
this.base.setToolbarPosition();
|
2358
|
+
|
2359
|
+
input.value = link_value || '';
|
2360
|
+
input.focus();
|
2361
|
+
},
|
2362
|
+
|
2363
|
+
// Called by core when tearing down medium-editor (deactivate)
|
2364
|
+
deactivate: function () {
|
2365
|
+
if (!this.form) {
|
2366
|
+
return false;
|
2367
|
+
}
|
2368
|
+
|
2369
|
+
if (this.form.parentNode) {
|
2370
|
+
this.form.parentNode.removeChild(this.form);
|
2371
|
+
}
|
2372
|
+
|
2373
|
+
delete this.form;
|
2374
|
+
},
|
2375
|
+
|
2376
|
+
// core methods
|
2377
|
+
|
2378
|
+
getFormOpts: function () {
|
2379
|
+
// no notion of private functions? wanted `_getFormOpts`
|
2380
|
+
var targetCheckbox = this.getForm().querySelector('.medium-editor-toolbar-anchor-target'),
|
2381
|
+
buttonCheckbox = this.getForm().querySelector('.medium-editor-toolbar-anchor-button'),
|
2382
|
+
opts = {
|
2383
|
+
url: this.getInput().value
|
2384
|
+
};
|
2385
|
+
|
2386
|
+
if (this.base.options.checkLinkFormat) {
|
2387
|
+
opts.url = this.checkLinkFormat(opts.url);
|
2388
|
+
}
|
2389
|
+
|
2390
|
+
if (targetCheckbox && targetCheckbox.checked) {
|
2391
|
+
opts.target = "_blank";
|
2392
|
+
} else {
|
2393
|
+
opts.target = "_self";
|
2394
|
+
}
|
2395
|
+
|
2396
|
+
if (buttonCheckbox && buttonCheckbox.checked) {
|
2397
|
+
opts.buttonClass = this.base.options.anchorButtonClass;
|
2398
|
+
}
|
2399
|
+
|
2400
|
+
return opts;
|
2401
|
+
},
|
2402
|
+
|
2403
|
+
doFormSave: function () {
|
2404
|
+
var opts = this.getFormOpts();
|
2405
|
+
this.completeFormSave(opts);
|
2406
|
+
},
|
2407
|
+
|
2408
|
+
completeFormSave: function (opts) {
|
2409
|
+
this.base.restoreSelection();
|
2410
|
+
this.base.createLink(opts);
|
2411
|
+
this.base.checkSelection();
|
2412
|
+
},
|
2413
|
+
|
2414
|
+
checkLinkFormat: function (value) {
|
2415
|
+
var re = /^(https?|ftps?|rtmpt?):\/\/|mailto:/;
|
2416
|
+
return (re.test(value) ? '' : 'http://') + value;
|
2417
|
+
},
|
2418
|
+
|
2419
|
+
doFormCancel: function () {
|
2420
|
+
this.base.restoreSelection();
|
2421
|
+
this.base.checkSelection();
|
2422
|
+
},
|
2423
|
+
|
2424
|
+
// form creation and event handling
|
2425
|
+
|
2426
|
+
attachFormEvents: function (form) {
|
2427
|
+
var close = form.querySelector(".medium-editor-toolbar-close"),
|
2428
|
+
save = form.querySelector(".medium-editor-toolbar-save"),
|
2429
|
+
input = form.querySelector(".medium-editor-toolbar-input");
|
2430
|
+
|
2431
|
+
// Handle clicks on the form itself
|
2432
|
+
this.base.on(form, 'click', this.handleFormClick.bind(this));
|
2433
|
+
|
2434
|
+
// Handle typing in the textbox
|
2435
|
+
this.base.on(input, 'keyup', this.handleTextboxKeyup.bind(this));
|
2436
|
+
|
2437
|
+
// Handle close button clicks
|
2438
|
+
this.base.on(close, 'click', this.handleCloseClick.bind(this));
|
2439
|
+
|
2440
|
+
// Handle save button clicks (capture)
|
2441
|
+
this.base.on(save, 'click', this.handleSaveClick.bind(this), true);
|
2442
|
+
|
2443
|
+
},
|
2444
|
+
|
2445
|
+
createForm: function () {
|
2446
|
+
var doc = this.base.options.ownerDocument,
|
2447
|
+
form = doc.createElement('div');
|
2448
|
+
|
2449
|
+
// Anchor Form (div)
|
2450
|
+
form.className = 'medium-editor-toolbar-form';
|
2451
|
+
form.id = 'medium-editor-toolbar-form-anchor-' + this.base.id;
|
2452
|
+
form.innerHTML = this.getTemplate();
|
2453
|
+
this.attachFormEvents(form);
|
2454
|
+
|
2455
|
+
return form;
|
2456
|
+
},
|
2457
|
+
|
2458
|
+
getInput: function () {
|
2459
|
+
return this.getForm().querySelector('input.medium-editor-toolbar-input');
|
2460
|
+
},
|
2461
|
+
|
2462
|
+
handleTextboxKeyup: function (event) {
|
2463
|
+
// For ENTER -> create the anchor
|
2464
|
+
if (event.keyCode === Util.keyCode.ENTER) {
|
2465
|
+
event.preventDefault();
|
2466
|
+
this.doFormSave();
|
2467
|
+
return;
|
2468
|
+
}
|
2469
|
+
|
2470
|
+
// For ESCAPE -> close the form
|
2471
|
+
if (event.keyCode === Util.keyCode.ESCAPE) {
|
2472
|
+
event.preventDefault();
|
2473
|
+
this.doFormCancel();
|
2474
|
+
}
|
2475
|
+
},
|
2476
|
+
|
2477
|
+
handleFormClick: function (event) {
|
2478
|
+
// make sure not to hide form when clicking inside the form
|
2479
|
+
event.stopPropagation();
|
2480
|
+
},
|
2481
|
+
|
2482
|
+
handleSaveClick: function (event) {
|
2483
|
+
// Clicking Save -> create the anchor
|
2484
|
+
event.preventDefault();
|
2485
|
+
this.doFormSave();
|
2486
|
+
},
|
2487
|
+
|
2488
|
+
handleCloseClick: function (event) {
|
2489
|
+
// Click Close -> close the form
|
2490
|
+
event.preventDefault();
|
2491
|
+
this.doFormCancel();
|
2492
|
+
}
|
2493
|
+
};
|
2494
|
+
|
2495
|
+
AnchorExtension = Util.derives(DefaultButton, AnchorDerived);
|
2496
|
+
|
2497
|
+
}());
|
2498
|
+
|
2499
|
+
var AnchorPreview;
|
2500
|
+
|
2501
|
+
(function () {
|
2502
|
+
'use strict';
|
2503
|
+
|
2504
|
+
AnchorPreview = function () {
|
2505
|
+
this.parent = true;
|
2506
|
+
this.name = 'anchor-preview';
|
2507
|
+
};
|
2508
|
+
|
2509
|
+
AnchorPreview.prototype = {
|
2510
|
+
|
2511
|
+
// the default selector to locate where to
|
2512
|
+
// put the activeAnchor value in the preview
|
2513
|
+
previewValueSelector: 'a',
|
2514
|
+
|
2515
|
+
init: function (instance) {
|
2516
|
+
|
2517
|
+
this.base = instance;
|
2518
|
+
this.anchorPreview = this.createPreview();
|
2519
|
+
this.base.options.elementsContainer.appendChild(this.anchorPreview);
|
2520
|
+
|
2521
|
+
this.attachToEditables();
|
2522
|
+
},
|
2523
|
+
|
2524
|
+
getPreviewElement: function () {
|
2525
|
+
return this.anchorPreview;
|
2526
|
+
},
|
2527
|
+
|
2528
|
+
createPreview: function () {
|
2529
|
+
var el = this.base.options.ownerDocument.createElement('div');
|
2530
|
+
|
2531
|
+
el.id = 'medium-editor-anchor-preview-' + this.base.id;
|
2532
|
+
el.className = 'medium-editor-anchor-preview';
|
2533
|
+
el.innerHTML = this.getTemplate();
|
2534
|
+
|
2535
|
+
this.base.on(el, 'click', this.handleClick.bind(this));
|
2536
|
+
|
2537
|
+
return el;
|
2538
|
+
},
|
2539
|
+
|
2540
|
+
getTemplate: function () {
|
2541
|
+
return '<div class="medium-editor-toolbar-anchor-preview" id="medium-editor-toolbar-anchor-preview">' +
|
2542
|
+
' <a class="medium-editor-toolbar-anchor-preview-inner"></a>' +
|
2543
|
+
'</div>';
|
2544
|
+
},
|
2545
|
+
|
2546
|
+
deactivate: function () {
|
2547
|
+
if (this.anchorPreview) {
|
2548
|
+
if (this.anchorPreview.parentNode) {
|
2549
|
+
this.anchorPreview.parentNode.removeChild(this.anchorPreview);
|
2550
|
+
}
|
2551
|
+
delete this.anchorPreview;
|
2552
|
+
}
|
2553
|
+
},
|
2554
|
+
|
2555
|
+
hidePreview: function () {
|
2556
|
+
this.anchorPreview.classList.remove('medium-editor-anchor-preview-active');
|
2557
|
+
this.activeAnchor = null;
|
2558
|
+
},
|
2559
|
+
|
2560
|
+
showPreview: function (anchorEl) {
|
2561
|
+
if (this.anchorPreview.classList.contains('medium-editor-anchor-preview-active') ||
|
2562
|
+
anchorEl.getAttribute('data-disable-preview')) {
|
2563
|
+
return true;
|
2564
|
+
}
|
2565
|
+
|
2566
|
+
if (this.previewValueSelector) {
|
2567
|
+
this.anchorPreview.querySelector(this.previewValueSelector).textContent = anchorEl.attributes.href.value;
|
2568
|
+
this.anchorPreview.querySelector(this.previewValueSelector).href = anchorEl.attributes.href.value;
|
2569
|
+
}
|
2570
|
+
|
2571
|
+
this.anchorPreview.classList.add('medium-toolbar-arrow-over');
|
2572
|
+
this.anchorPreview.classList.remove('medium-toolbar-arrow-under');
|
2573
|
+
|
2574
|
+
if (!this.anchorPreview.classList.contains('medium-editor-anchor-preview-active')) {
|
2575
|
+
this.anchorPreview.classList.add('medium-editor-anchor-preview-active');
|
2576
|
+
}
|
2577
|
+
|
2578
|
+
this.activeAnchor = anchorEl;
|
2579
|
+
|
2580
|
+
this.positionPreview();
|
2581
|
+
this.attachPreviewHandlers();
|
2582
|
+
|
2583
|
+
return this;
|
2584
|
+
},
|
2585
|
+
|
2586
|
+
positionPreview: function () {
|
2587
|
+
var buttonHeight = 40,
|
2588
|
+
boundary = this.activeAnchor.getBoundingClientRect(),
|
2589
|
+
middleBoundary = (boundary.left + boundary.right) / 2,
|
2590
|
+
halfOffsetWidth,
|
2591
|
+
defaultLeft;
|
2592
|
+
|
2593
|
+
halfOffsetWidth = this.anchorPreview.offsetWidth / 2;
|
2594
|
+
defaultLeft = this.base.options.diffLeft - halfOffsetWidth;
|
2595
|
+
|
2596
|
+
this.anchorPreview.style.top = Math.round(buttonHeight + boundary.bottom - this.base.options.diffTop + this.base.options.contentWindow.pageYOffset - this.anchorPreview.offsetHeight) + 'px';
|
2597
|
+
if (middleBoundary < halfOffsetWidth) {
|
2598
|
+
this.anchorPreview.style.left = defaultLeft + halfOffsetWidth + 'px';
|
2599
|
+
} else if ((this.base.options.contentWindow.innerWidth - middleBoundary) < halfOffsetWidth) {
|
2600
|
+
this.anchorPreview.style.left = this.base.options.contentWindow.innerWidth + defaultLeft - halfOffsetWidth + 'px';
|
2601
|
+
} else {
|
2602
|
+
this.anchorPreview.style.left = defaultLeft + middleBoundary + 'px';
|
2603
|
+
}
|
2604
|
+
},
|
2605
|
+
|
2606
|
+
attachToEditables: function () {
|
2607
|
+
this.base.subscribe('editableMouseover', this.handleEditableMouseover.bind(this));
|
2608
|
+
},
|
2609
|
+
|
2610
|
+
handleClick: function (event) {
|
2611
|
+
var anchorExtension = this.base.getExtensionByName('anchor'),
|
2612
|
+
activeAnchor = this.activeAnchor;
|
2613
|
+
|
2614
|
+
if (anchorExtension && activeAnchor) {
|
2615
|
+
event.preventDefault();
|
2616
|
+
|
2617
|
+
this.base.selectElement(this.activeAnchor);
|
2618
|
+
|
2619
|
+
// Using setTimeout + options.delay because:
|
2620
|
+
// We may actually be displaying the anchor form, which should be controlled by options.delay
|
2621
|
+
this.base.delay(function () {
|
2622
|
+
if (activeAnchor) {
|
2623
|
+
anchorExtension.showForm(activeAnchor.attributes.href.value);
|
2624
|
+
activeAnchor = null;
|
2625
|
+
}
|
2626
|
+
}.bind(this));
|
2627
|
+
}
|
2628
|
+
|
2629
|
+
this.hidePreview();
|
2630
|
+
},
|
2631
|
+
|
2632
|
+
handleAnchorMouseout: function () {
|
2633
|
+
this.anchorToPreview = null;
|
2634
|
+
this.base.off(this.activeAnchor, 'mouseout', this.instance_handleAnchorMouseout);
|
2635
|
+
this.instance_handleAnchorMouseout = null;
|
2636
|
+
},
|
2637
|
+
|
2638
|
+
|
2639
|
+
handleEditableMouseover: function (event) {
|
2640
|
+
var target = Util.getClosestTag(event.target, 'a');
|
2641
|
+
|
2642
|
+
if (target) {
|
2643
|
+
|
2644
|
+
// Detect empty href attributes
|
2645
|
+
// The browser will make href="" or href="#top"
|
2646
|
+
// into absolute urls when accessed as event.targed.href, so check the html
|
2647
|
+
if (!/href=["']\S+["']/.test(target.outerHTML) || /href=["']#\S+["']/.test(target.outerHTML)) {
|
2648
|
+
return true;
|
2649
|
+
}
|
2650
|
+
|
2651
|
+
// only show when hovering on anchors
|
2652
|
+
if (this.base.toolbar && this.base.toolbar.isDisplayed()) {
|
2653
|
+
// only show when toolbar is not present
|
2654
|
+
return true;
|
2655
|
+
}
|
2656
|
+
|
2657
|
+
// detach handler for other anchor in case we hovered multiple anchors quickly
|
2658
|
+
if (this.activeAnchor && this.activeAnchor !== target) {
|
2659
|
+
this.detachPreviewHandlers();
|
2660
|
+
}
|
2661
|
+
|
2662
|
+
this.anchorToPreview = target;
|
2663
|
+
|
2664
|
+
this.instance_handleAnchorMouseout = this.handleAnchorMouseout.bind(this);
|
2665
|
+
this.base.on(this.anchorToPreview, 'mouseout', this.instance_handleAnchorMouseout);
|
2666
|
+
// Using setTimeout + options.delay because:
|
2667
|
+
// - We're going to show the anchor preview according to the configured delay
|
2668
|
+
// if the mouse has not left the anchor tag in that time
|
2669
|
+
this.base.delay(function () {
|
2670
|
+
if (this.anchorToPreview) {
|
2671
|
+
//this.activeAnchor = this.anchorToPreview;
|
2672
|
+
this.showPreview(this.anchorToPreview);
|
2673
|
+
}
|
2674
|
+
}.bind(this));
|
2675
|
+
}
|
2676
|
+
},
|
2677
|
+
|
2678
|
+
handlePreviewMouseover: function () {
|
2679
|
+
this.lastOver = (new Date()).getTime();
|
2680
|
+
this.hovering = true;
|
2681
|
+
},
|
2682
|
+
|
2683
|
+
handlePreviewMouseout: function (event) {
|
2684
|
+
if (!event.relatedTarget || !/anchor-preview/.test(event.relatedTarget.className)) {
|
2685
|
+
this.hovering = false;
|
2686
|
+
}
|
2687
|
+
},
|
2688
|
+
|
2689
|
+
updatePreview: function () {
|
2690
|
+
if (this.hovering) {
|
2691
|
+
return true;
|
2692
|
+
}
|
2693
|
+
var durr = (new Date()).getTime() - this.lastOver;
|
2694
|
+
if (durr > this.base.options.anchorPreviewHideDelay) {
|
2695
|
+
// hide the preview 1/2 second after mouse leaves the link
|
2696
|
+
this.detachPreviewHandlers();
|
2697
|
+
}
|
2698
|
+
},
|
2699
|
+
|
2700
|
+
detachPreviewHandlers: function () {
|
2701
|
+
// cleanup
|
2702
|
+
clearInterval(this.interval_timer);
|
2703
|
+
if (this.instance_handlePreviewMouseover) {
|
2704
|
+
this.base.off(this.anchorPreview, 'mouseover', this.instance_handlePreviewMouseover);
|
2705
|
+
this.base.off(this.anchorPreview, 'mouseout', this.instance_handlePreviewMouseout);
|
2706
|
+
if (this.activeAnchor) {
|
2707
|
+
this.base.off(this.activeAnchor, 'mouseover', this.instance_handlePreviewMouseover);
|
2708
|
+
this.base.off(this.activeAnchor, 'mouseout', this.instance_handlePreviewMouseout);
|
2709
|
+
}
|
2710
|
+
}
|
2711
|
+
|
2712
|
+
this.hidePreview();
|
2713
|
+
|
2714
|
+
this.hovering = this.instance_handlePreviewMouseover = this.instance_handlePreviewMouseout = null;
|
2715
|
+
},
|
2716
|
+
|
2717
|
+
// TODO: break up method and extract out handlers
|
2718
|
+
attachPreviewHandlers: function () {
|
2719
|
+
this.lastOver = (new Date()).getTime();
|
2720
|
+
this.hovering = true;
|
2721
|
+
|
2722
|
+
this.instance_handlePreviewMouseover = this.handlePreviewMouseover.bind(this);
|
2723
|
+
this.instance_handlePreviewMouseout = this.handlePreviewMouseout.bind(this);
|
2724
|
+
|
2725
|
+
this.interval_timer = setInterval(this.updatePreview.bind(this), 200);
|
2726
|
+
|
2727
|
+
this.base.on(this.anchorPreview, 'mouseover', this.instance_handlePreviewMouseover);
|
2728
|
+
this.base.on(this.anchorPreview, 'mouseout', this.instance_handlePreviewMouseout);
|
2729
|
+
this.base.on(this.activeAnchor, 'mouseover', this.instance_handlePreviewMouseover);
|
2730
|
+
this.base.on(this.activeAnchor, 'mouseout', this.instance_handlePreviewMouseout);
|
2731
|
+
}
|
2732
|
+
};
|
2733
|
+
}());
|
2734
|
+
|
2735
|
+
var FontSizeExtension;
|
2736
|
+
|
2737
|
+
(function () {
|
2738
|
+
'use strict';
|
2739
|
+
|
2740
|
+
function FontSizeDerived() {
|
2741
|
+
this.parent = true;
|
2742
|
+
this.options = {
|
2743
|
+
name: 'fontsize',
|
2744
|
+
action: 'fontSize',
|
2745
|
+
aria: 'increase/decrease font size',
|
2746
|
+
contentDefault: '±', // ±
|
2747
|
+
contentFA: '<i class="fa fa-text-height"></i>'
|
2748
|
+
};
|
2749
|
+
this.name = 'fontsize';
|
2750
|
+
this.hasForm = true;
|
2751
|
+
}
|
2752
|
+
|
2753
|
+
FontSizeDerived.prototype = {
|
2754
|
+
|
2755
|
+
// Button and Extension handling
|
2756
|
+
|
2757
|
+
// Called when the button the toolbar is clicked
|
2758
|
+
// Overrides DefaultButton.handleClick
|
2759
|
+
handleClick: function (evt) {
|
2760
|
+
evt.preventDefault();
|
2761
|
+
evt.stopPropagation();
|
2762
|
+
|
2763
|
+
if (!this.isDisplayed()) {
|
2764
|
+
// Get fontsize of current selection (convert to string since IE returns this as number)
|
2765
|
+
var fontSize = this.base.options.ownerDocument.queryCommandValue('fontSize') + '';
|
2766
|
+
this.showForm(fontSize);
|
2767
|
+
}
|
2768
|
+
|
2769
|
+
return false;
|
2770
|
+
},
|
2771
|
+
|
2772
|
+
// Called by medium-editor to append form to the toolbar
|
2773
|
+
getForm: function () {
|
2774
|
+
if (!this.form) {
|
2775
|
+
this.form = this.createForm();
|
2776
|
+
}
|
2777
|
+
return this.form;
|
2778
|
+
},
|
2779
|
+
|
2780
|
+
// Used by medium-editor when the default toolbar is to be displayed
|
2781
|
+
isDisplayed: function () {
|
2782
|
+
return this.getForm().style.display === 'block';
|
2783
|
+
},
|
2784
|
+
|
2785
|
+
hideForm: function () {
|
2786
|
+
this.getForm().style.display = 'none';
|
2787
|
+
this.getInput().value = '';
|
2788
|
+
},
|
2789
|
+
|
2790
|
+
showForm: function (fontSize) {
|
2791
|
+
var input = this.getInput();
|
2792
|
+
|
2793
|
+
this.base.saveSelection();
|
2794
|
+
this.base.hideToolbarDefaultActions();
|
2795
|
+
this.getForm().style.display = 'block';
|
2796
|
+
this.base.setToolbarPosition();
|
2797
|
+
|
2798
|
+
input.value = fontSize || '';
|
2799
|
+
input.focus();
|
2800
|
+
},
|
2801
|
+
|
2802
|
+
// Called by core when tearing down medium-editor (deactivate)
|
2803
|
+
deactivate: function () {
|
2804
|
+
if (!this.form) {
|
2805
|
+
return false;
|
2806
|
+
}
|
2807
|
+
|
2808
|
+
if (this.form.parentNode) {
|
2809
|
+
this.form.parentNode.removeChild(this.form);
|
2810
|
+
}
|
2811
|
+
|
2812
|
+
delete this.form;
|
2813
|
+
},
|
2814
|
+
|
2815
|
+
// core methods
|
2816
|
+
|
2817
|
+
doFormSave: function () {
|
2818
|
+
this.base.restoreSelection();
|
2819
|
+
this.base.checkSelection();
|
2820
|
+
},
|
2821
|
+
|
2822
|
+
doFormCancel: function () {
|
2823
|
+
this.base.restoreSelection();
|
2824
|
+
this.clearFontSize();
|
2825
|
+
this.base.checkSelection();
|
2826
|
+
},
|
2827
|
+
|
2828
|
+
// form creation and event handling
|
2829
|
+
|
2830
|
+
createForm: function () {
|
2831
|
+
var doc = this.base.options.ownerDocument,
|
2832
|
+
form = doc.createElement('div'),
|
2833
|
+
input = doc.createElement('input'),
|
2834
|
+
close = doc.createElement('a'),
|
2835
|
+
save = doc.createElement('a');
|
2836
|
+
|
2837
|
+
// Font Size Form (div)
|
2838
|
+
form.className = 'medium-editor-toolbar-form';
|
2839
|
+
form.id = 'medium-editor-toolbar-form-fontsize-' + this.base.id;
|
2840
|
+
|
2841
|
+
// Handle clicks on the form itself
|
2842
|
+
this.base.on(form, 'click', this.handleFormClick.bind(this));
|
2843
|
+
|
2844
|
+
// Add font size slider
|
2845
|
+
input.setAttribute('type', 'range');
|
2846
|
+
input.setAttribute('min', '1');
|
2847
|
+
input.setAttribute('max', '7');
|
2848
|
+
input.className = 'medium-editor-toolbar-input';
|
2849
|
+
form.appendChild(input);
|
2850
|
+
|
2851
|
+
// Handle typing in the textbox
|
2852
|
+
this.base.on(input, 'change', this.handleSliderChange.bind(this));
|
2853
|
+
|
2854
|
+
// Add save buton
|
2855
|
+
save.setAttribute('href', '#');
|
2856
|
+
save.className = 'medium-editor-toobar-save';
|
2857
|
+
save.innerHTML = this.base.options.buttonLabels === 'fontawesome' ?
|
2858
|
+
'<i class="fa fa-check"></i>' :
|
2859
|
+
'✓';
|
2860
|
+
form.appendChild(save);
|
2861
|
+
|
2862
|
+
// Handle save button clicks (capture)
|
2863
|
+
this.base.on(save, 'click', this.handleSaveClick.bind(this), true);
|
2864
|
+
|
2865
|
+
// Add close button
|
2866
|
+
close.setAttribute('href', '#');
|
2867
|
+
close.className = 'medium-editor-toobar-close';
|
2868
|
+
close.innerHTML = this.base.options.buttonLabels === 'fontawesome' ?
|
2869
|
+
'<i class="fa fa-times"></i>' :
|
2870
|
+
'×';
|
2871
|
+
form.appendChild(close);
|
2872
|
+
|
2873
|
+
// Handle close button clicks
|
2874
|
+
this.base.on(close, 'click', this.handleCloseClick.bind(this));
|
2875
|
+
|
2876
|
+
return form;
|
2877
|
+
},
|
2878
|
+
|
2879
|
+
getInput: function () {
|
2880
|
+
return this.getForm().querySelector('input.medium-editor-toolbar-input');
|
2881
|
+
},
|
2882
|
+
|
2883
|
+
clearFontSize: function () {
|
2884
|
+
Selection.getSelectedElements(this.base.options.ownerDocument).forEach(function (el) {
|
2885
|
+
if (el.tagName === 'FONT' && el.hasAttribute('size')) {
|
2886
|
+
el.removeAttribute('size');
|
2887
|
+
}
|
2888
|
+
});
|
2889
|
+
},
|
2890
|
+
|
2891
|
+
handleSliderChange: function () {
|
2892
|
+
var size = this.getInput().value;
|
2893
|
+
if (size === '4') {
|
2894
|
+
this.clearFontSize();
|
2895
|
+
} else {
|
2896
|
+
this.base.execAction('fontSize', { size: size });
|
2897
|
+
}
|
2898
|
+
},
|
2899
|
+
|
2900
|
+
handleFormClick: function (event) {
|
2901
|
+
// make sure not to hide form when clicking inside the form
|
2902
|
+
event.stopPropagation();
|
2903
|
+
},
|
2904
|
+
|
2905
|
+
handleSaveClick: function (event) {
|
2906
|
+
// Clicking Save -> create the font size
|
2907
|
+
event.preventDefault();
|
2908
|
+
this.doFormSave();
|
2909
|
+
},
|
2910
|
+
|
2911
|
+
handleCloseClick: function (event) {
|
2912
|
+
// Click Close -> close the form
|
2913
|
+
event.preventDefault();
|
2914
|
+
this.doFormCancel();
|
2915
|
+
}
|
2916
|
+
};
|
2917
|
+
|
2918
|
+
FontSizeExtension = Util.derives(DefaultButton, FontSizeDerived);
|
2919
|
+
}());
|
2920
|
+
|
2921
|
+
var Toolbar;
|
2922
|
+
|
2923
|
+
(function () {
|
2924
|
+
'use strict';
|
2925
|
+
|
2926
|
+
Toolbar = function Toolbar(instance) {
|
2927
|
+
this.base = instance;
|
2928
|
+
this.options = instance.options;
|
2929
|
+
this.initThrottledMethods();
|
2930
|
+
};
|
2931
|
+
|
2932
|
+
Toolbar.prototype = {
|
2933
|
+
|
2934
|
+
// Toolbar creation/deletion
|
2935
|
+
|
2936
|
+
createToolbar: function () {
|
2937
|
+
var toolbar = this.base.options.ownerDocument.createElement('div');
|
2938
|
+
|
2939
|
+
toolbar.id = 'medium-editor-toolbar-' + this.base.id;
|
2940
|
+
toolbar.className = 'medium-editor-toolbar';
|
2941
|
+
|
2942
|
+
if (this.options.staticToolbar) {
|
2943
|
+
toolbar.className += " static-toolbar";
|
2944
|
+
} else {
|
2945
|
+
toolbar.className += " stalker-toolbar";
|
2946
|
+
}
|
2947
|
+
|
2948
|
+
|
2949
|
+
toolbar.appendChild(this.createToolbarButtons());
|
2950
|
+
|
2951
|
+
// Add any forms that extensions may have
|
2952
|
+
this.base.commands.forEach(function (extension) {
|
2953
|
+
if (extension.hasForm) {
|
2954
|
+
toolbar.appendChild(extension.getForm());
|
2955
|
+
}
|
2956
|
+
});
|
2957
|
+
|
2958
|
+
this.attachEventHandlers();
|
2959
|
+
|
2960
|
+
return toolbar;
|
2961
|
+
},
|
2962
|
+
|
2963
|
+
createToolbarButtons: function () {
|
2964
|
+
var ul = this.base.options.ownerDocument.createElement('ul'),
|
2965
|
+
li,
|
2966
|
+
btn,
|
2967
|
+
buttons,
|
2968
|
+
extension;
|
2969
|
+
|
2970
|
+
ul.id = 'medium-editor-toolbar-actions' + this.base.id;
|
2971
|
+
ul.className = 'medium-editor-toolbar-actions clearfix';
|
2972
|
+
ul.style.display = 'block';
|
2973
|
+
|
2974
|
+
this.base.options.buttons.forEach(function (button) {
|
2975
|
+
extension = this.base.getExtensionByName(button);
|
2976
|
+
if (typeof extension.getButton === 'function') {
|
2977
|
+
btn = extension.getButton(this.base);
|
2978
|
+
li = this.base.options.ownerDocument.createElement('li');
|
2979
|
+
if (Util.isElement(btn)) {
|
2980
|
+
li.appendChild(btn);
|
2981
|
+
} else {
|
2982
|
+
li.innerHTML = btn;
|
2983
|
+
}
|
2984
|
+
ul.appendChild(li);
|
2985
|
+
}
|
2986
|
+
}.bind(this));
|
2987
|
+
|
2988
|
+
buttons = ul.querySelectorAll('button');
|
2989
|
+
if (buttons.length > 0) {
|
2990
|
+
buttons[0].classList.add(this.options.firstButtonClass);
|
2991
|
+
buttons[buttons.length - 1].classList.add(this.options.lastButtonClass);
|
2992
|
+
}
|
2993
|
+
|
2994
|
+
return ul;
|
2995
|
+
},
|
2996
|
+
|
2997
|
+
deactivate: function () {
|
2998
|
+
if (this.toolbar) {
|
2999
|
+
if (this.toolbar.parentNode) {
|
3000
|
+
this.toolbar.parentNode.removeChild(this.toolbar);
|
3001
|
+
}
|
3002
|
+
delete this.toolbar;
|
3003
|
+
}
|
3004
|
+
},
|
3005
|
+
|
3006
|
+
// Toolbar accessors
|
3007
|
+
|
3008
|
+
getToolbarElement: function () {
|
3009
|
+
if (!this.toolbar) {
|
3010
|
+
this.toolbar = this.createToolbar();
|
3011
|
+
}
|
3012
|
+
|
3013
|
+
return this.toolbar;
|
3014
|
+
},
|
3015
|
+
|
3016
|
+
getToolbarActionsElement: function () {
|
3017
|
+
return this.getToolbarElement().querySelector('.medium-editor-toolbar-actions');
|
3018
|
+
},
|
3019
|
+
|
3020
|
+
// Toolbar event handlers
|
3021
|
+
|
3022
|
+
initThrottledMethods: function () {
|
3023
|
+
// throttledPositionToolbar is throttled because:
|
3024
|
+
// - It will be called when the browser is resizing, which can fire many times very quickly
|
3025
|
+
// - For some event (like resize) a slight lag in UI responsiveness is OK and provides performance benefits
|
3026
|
+
this.throttledPositionToolbar = Util.throttle(function () {
|
3027
|
+
if (this.base.isActive) {
|
3028
|
+
this.positionToolbarIfShown();
|
3029
|
+
}
|
3030
|
+
}.bind(this));
|
3031
|
+
},
|
3032
|
+
|
3033
|
+
attachEventHandlers: function () {
|
3034
|
+
|
3035
|
+
// MediumEditor custom events for when user beings and ends interaction with a contenteditable and its elements
|
3036
|
+
this.base.subscribe('blur', this.handleBlur.bind(this));
|
3037
|
+
this.base.subscribe('focus', this.handleFocus.bind(this));
|
3038
|
+
|
3039
|
+
// Updating the state of the toolbar as things change
|
3040
|
+
this.base.subscribe('editableClick', this.handleEditableClick.bind(this));
|
3041
|
+
this.base.subscribe('editableKeyup', this.handleEditableKeyup.bind(this));
|
3042
|
+
|
3043
|
+
// Handle mouseup on document for updating the selection in the toolbar
|
3044
|
+
this.base.on(this.options.ownerDocument.documentElement, 'mouseup', this.handleDocumentMouseup.bind(this));
|
3045
|
+
|
3046
|
+
// Add a scroll event for sticky toolbar
|
3047
|
+
if (this.options.staticToolbar && this.options.stickyToolbar) {
|
3048
|
+
// On scroll (capture), re-position the toolbar
|
3049
|
+
this.base.on(this.options.contentWindow, 'scroll', this.handleWindowScroll.bind(this), true);
|
3050
|
+
}
|
3051
|
+
|
3052
|
+
// On resize, re-position the toolbar
|
3053
|
+
this.base.on(this.options.contentWindow, 'resize', this.handleWindowResize.bind(this));
|
3054
|
+
},
|
3055
|
+
|
3056
|
+
handleWindowScroll: function () {
|
3057
|
+
this.positionToolbarIfShown();
|
3058
|
+
},
|
3059
|
+
|
3060
|
+
handleWindowResize: function () {
|
3061
|
+
this.throttledPositionToolbar();
|
3062
|
+
},
|
3063
|
+
|
3064
|
+
handleDocumentMouseup: function (event) {
|
3065
|
+
// Do not trigger checkState when mouseup fires over the toolbar
|
3066
|
+
if (event &&
|
3067
|
+
event.target &&
|
3068
|
+
Util.isDescendant(this.getToolbarElement(), event.target)) {
|
3069
|
+
return false;
|
3070
|
+
}
|
3071
|
+
this.checkState();
|
3072
|
+
},
|
3073
|
+
|
3074
|
+
handleEditableClick: function () {
|
3075
|
+
// Delay the call to checkState to handle bug where selection is empty
|
3076
|
+
// immediately after clicking inside a pre-existing selection
|
3077
|
+
setTimeout(function () {
|
3078
|
+
this.checkState();
|
3079
|
+
}.bind(this), 0);
|
3080
|
+
},
|
3081
|
+
|
3082
|
+
handleEditableKeyup: function () {
|
3083
|
+
this.checkState();
|
3084
|
+
},
|
3085
|
+
|
3086
|
+
handleBlur: function () {
|
3087
|
+
// Kill any previously delayed calls to hide the toolbar
|
3088
|
+
clearTimeout(this.hideTimeout);
|
3089
|
+
|
3090
|
+
// Blur may fire even if we have a selection, so we want to prevent any delayed showToolbar
|
3091
|
+
// calls from happening in this specific case
|
3092
|
+
clearTimeout(this.delayShowTimeout);
|
3093
|
+
|
3094
|
+
// Delay the call to hideToolbar to handle bug with multiple editors on the page at once
|
3095
|
+
this.hideTimeout = setTimeout(function () {
|
3096
|
+
this.hideToolbar();
|
3097
|
+
}.bind(this), 1);
|
3098
|
+
},
|
3099
|
+
|
3100
|
+
handleFocus: function () {
|
3101
|
+
this.checkState();
|
3102
|
+
},
|
3103
|
+
|
3104
|
+
// Hiding/showing toolbar
|
3105
|
+
|
3106
|
+
isDisplayed: function () {
|
3107
|
+
return this.getToolbarElement().classList.contains('medium-editor-toolbar-active');
|
3108
|
+
},
|
3109
|
+
|
3110
|
+
showToolbar: function () {
|
3111
|
+
clearTimeout(this.hideTimeout);
|
3112
|
+
if (!this.isDisplayed()) {
|
3113
|
+
this.getToolbarElement().classList.add('medium-editor-toolbar-active');
|
3114
|
+
if (typeof this.options.onShowToolbar === 'function') {
|
3115
|
+
this.options.onShowToolbar();
|
3116
|
+
}
|
3117
|
+
}
|
3118
|
+
},
|
3119
|
+
|
3120
|
+
hideToolbar: function () {
|
3121
|
+
if (this.isDisplayed()) {
|
3122
|
+
this.base.commands.forEach(function (extension) {
|
3123
|
+
if (typeof extension.onHide === 'function') {
|
3124
|
+
extension.onHide();
|
3125
|
+
}
|
3126
|
+
});
|
3127
|
+
|
3128
|
+
this.getToolbarElement().classList.remove('medium-editor-toolbar-active');
|
3129
|
+
if (typeof this.options.onHideToolbar === 'function') {
|
3130
|
+
this.options.onHideToolbar();
|
3131
|
+
}
|
3132
|
+
}
|
3133
|
+
},
|
3134
|
+
|
3135
|
+
isToolbarDefaultActionsDisplayed: function () {
|
3136
|
+
return this.getToolbarActionsElement().style.display === 'block';
|
3137
|
+
},
|
3138
|
+
|
3139
|
+
hideToolbarDefaultActions: function () {
|
3140
|
+
if (this.isToolbarDefaultActionsDisplayed()) {
|
3141
|
+
this.getToolbarActionsElement().style.display = 'none';
|
3142
|
+
}
|
3143
|
+
},
|
3144
|
+
|
3145
|
+
showToolbarDefaultActions: function () {
|
3146
|
+
this.hideExtensionForms();
|
3147
|
+
|
3148
|
+
if (!this.isToolbarDefaultActionsDisplayed()) {
|
3149
|
+
this.getToolbarActionsElement().style.display = 'block';
|
3150
|
+
}
|
3151
|
+
|
3152
|
+
// Using setTimeout + options.delay because:
|
3153
|
+
// We will actually be displaying the toolbar, which should be controlled by options.delay
|
3154
|
+
this.delayShowTimeout = this.base.delay(function () {
|
3155
|
+
this.showToolbar();
|
3156
|
+
}.bind(this));
|
3157
|
+
},
|
3158
|
+
|
3159
|
+
hideExtensionForms: function () {
|
3160
|
+
// Hide all extension forms
|
3161
|
+
this.base.commands.forEach(function (extension) {
|
3162
|
+
if (extension.hasForm && extension.isDisplayed()) {
|
3163
|
+
extension.hideForm();
|
3164
|
+
}
|
3165
|
+
});
|
3166
|
+
},
|
3167
|
+
|
3168
|
+
// Responding to changes in user selection
|
3169
|
+
|
3170
|
+
// Checks for existance of multiple block elements in the current selection
|
3171
|
+
multipleBlockElementsSelected: function () {
|
3172
|
+
/*jslint regexp: true*/
|
3173
|
+
var selectionHtml = Selection.getSelectionHtml.call(this).replace(/<[\S]+><\/[\S]+>/gim, ''),
|
3174
|
+
hasMultiParagraphs = selectionHtml.match(/<(p|h[1-6]|blockquote)[^>]*>/g);
|
3175
|
+
/*jslint regexp: false*/
|
3176
|
+
|
3177
|
+
return !!hasMultiParagraphs && hasMultiParagraphs.length > 1;
|
3178
|
+
},
|
3179
|
+
|
3180
|
+
modifySelection: function () {
|
3181
|
+
var selection = this.options.contentWindow.getSelection(),
|
3182
|
+
selectionRange = selection.getRangeAt(0);
|
3183
|
+
|
3184
|
+
/*
|
3185
|
+
* In firefox, there are cases (ie doubleclick of a word) where the selectionRange start
|
3186
|
+
* will be at the very end of an element. In other browsers, the selectionRange start
|
3187
|
+
* would instead be at the very beginning of an element that actually has content.
|
3188
|
+
* example:
|
3189
|
+
* <span>foo</span><span>bar</span>
|
3190
|
+
*
|
3191
|
+
* If the text 'bar' is selected, most browsers will have the selectionRange start at the beginning
|
3192
|
+
* of the 'bar' span. However, there are cases where firefox will have the selectionRange start
|
3193
|
+
* at the end of the 'foo' span. The contenteditable behavior will be ok, but if there are any
|
3194
|
+
* properties on the 'bar' span, they won't be reflected accurately in the toolbar
|
3195
|
+
* (ie 'Bold' button wouldn't be active)
|
3196
|
+
*
|
3197
|
+
* So, for cases where the selectionRange start is at the end of an element/node, find the next
|
3198
|
+
* adjacent text node that actually has content in it, and move the selectionRange start there.
|
3199
|
+
*/
|
3200
|
+
if (this.options.standardizeSelectionStart &&
|
3201
|
+
selectionRange.startContainer.nodeValue &&
|
3202
|
+
(selectionRange.startOffset === selectionRange.startContainer.nodeValue.length)) {
|
3203
|
+
var adjacentNode = Util.findAdjacentTextNodeWithContent(Selection.getSelectionElement(this.options.contentWindow), selectionRange.startContainer, this.options.ownerDocument);
|
3204
|
+
if (adjacentNode) {
|
3205
|
+
var offset = 0;
|
3206
|
+
while (adjacentNode.nodeValue.substr(offset, 1).trim().length === 0) {
|
3207
|
+
offset = offset + 1;
|
3208
|
+
}
|
3209
|
+
var newRange = this.options.ownerDocument.createRange();
|
3210
|
+
newRange.setStart(adjacentNode, offset);
|
3211
|
+
newRange.setEnd(selectionRange.endContainer, selectionRange.endOffset);
|
3212
|
+
selection.removeAllRanges();
|
3213
|
+
selection.addRange(newRange);
|
3214
|
+
selectionRange = newRange;
|
3215
|
+
}
|
3216
|
+
}
|
3217
|
+
},
|
3218
|
+
|
3219
|
+
checkState: function () {
|
3220
|
+
|
3221
|
+
if (!this.base.preventSelectionUpdates) {
|
3222
|
+
|
3223
|
+
// If no editable has focus OR selection is inside contenteditable = false
|
3224
|
+
// hide toolbar
|
3225
|
+
if (!this.getFocusedElement() ||
|
3226
|
+
Selection.selectionInContentEditableFalse(this.options.contentWindow)) {
|
3227
|
+
this.hideToolbar();
|
3228
|
+
return;
|
3229
|
+
}
|
3230
|
+
|
3231
|
+
// If there's no selection element, selection element doesn't belong to this editor
|
3232
|
+
// or toolbar is disabled for this selection element
|
3233
|
+
// hide toolbar
|
3234
|
+
var selectionElement = Selection.getSelectionElement(this.options.contentWindow);
|
3235
|
+
if (!selectionElement ||
|
3236
|
+
this.base.elements.indexOf(selectionElement) === -1 ||
|
3237
|
+
selectionElement.getAttribute('data-disable-toolbar')) {
|
3238
|
+
this.hideToolbar();
|
3239
|
+
return;
|
3240
|
+
}
|
3241
|
+
|
3242
|
+
// Now we know there's a focused editable with a selection
|
3243
|
+
|
3244
|
+
// If the updateOnEmptySelection option is true, show the toolbar
|
3245
|
+
if (this.options.updateOnEmptySelection && this.options.staticToolbar) {
|
3246
|
+
this.showAndUpdateToolbar();
|
3247
|
+
return;
|
3248
|
+
}
|
3249
|
+
|
3250
|
+
// If we don't have a "valid" selection -> hide toolbar
|
3251
|
+
if (this.options.contentWindow.getSelection().toString().trim() === '' ||
|
3252
|
+
(this.options.allowMultiParagraphSelection === false && this.multipleBlockElementsSelected())) {
|
3253
|
+
this.hideToolbar();
|
3254
|
+
} else {
|
3255
|
+
this.showAndUpdateToolbar();
|
3256
|
+
}
|
3257
|
+
}
|
3258
|
+
},
|
3259
|
+
|
3260
|
+
getFocusedElement: function () {
|
3261
|
+
for (var i = 0; i < this.base.elements.length; i += 1) {
|
3262
|
+
if (this.base.elements[i].getAttribute('data-medium-focused')) {
|
3263
|
+
return this.base.elements[i];
|
3264
|
+
}
|
3265
|
+
}
|
3266
|
+
return null;
|
3267
|
+
},
|
3268
|
+
|
3269
|
+
// Updating the toolbar
|
3270
|
+
|
3271
|
+
showAndUpdateToolbar: function () {
|
3272
|
+
this.modifySelection();
|
3273
|
+
this.setToolbarButtonStates();
|
3274
|
+
this.showToolbarDefaultActions();
|
3275
|
+
this.setToolbarPosition();
|
3276
|
+
},
|
3277
|
+
|
3278
|
+
setToolbarButtonStates: function () {
|
3279
|
+
this.base.commands.forEach(function (extension) {
|
3280
|
+
if (typeof extension.isActive === 'function' &&
|
3281
|
+
typeof extension.setInactive === 'function') {
|
3282
|
+
extension.setInactive();
|
3283
|
+
}
|
3284
|
+
}.bind(this));
|
3285
|
+
this.checkActiveButtons();
|
3286
|
+
},
|
3287
|
+
|
3288
|
+
checkActiveButtons: function () {
|
3289
|
+
var manualStateChecks = [],
|
3290
|
+
queryState = null,
|
3291
|
+
selectionRange = Util.getSelectionRange(this.options.ownerDocument),
|
3292
|
+
parentNode,
|
3293
|
+
updateExtensionState = function (extension) {
|
3294
|
+
if (typeof extension.checkState === 'function') {
|
3295
|
+
extension.checkState(parentNode);
|
3296
|
+
} else if (typeof extension.isActive === 'function' &&
|
3297
|
+
typeof extension.isAlreadyApplied === 'function' &&
|
3298
|
+
typeof extension.setActive === 'function') {
|
3299
|
+
if (!extension.isActive() && extension.isAlreadyApplied(parentNode)) {
|
3300
|
+
extension.setActive();
|
3301
|
+
}
|
3302
|
+
}
|
3303
|
+
};
|
3304
|
+
|
3305
|
+
if (!selectionRange) {
|
3306
|
+
return;
|
3307
|
+
}
|
3308
|
+
|
3309
|
+
parentNode = Selection.getSelectedParentElement(selectionRange);
|
3310
|
+
|
3311
|
+
// Loop through all commands
|
3312
|
+
this.base.commands.forEach(function (command) {
|
3313
|
+
// For those commands where we can use document.queryCommandState(), do so
|
3314
|
+
if (typeof command.queryCommandState === 'function') {
|
3315
|
+
queryState = command.queryCommandState();
|
3316
|
+
// If queryCommandState returns a valid value, we can trust the browser
|
3317
|
+
// and don't need to do our manual checks
|
3318
|
+
if (queryState !== null) {
|
3319
|
+
if (queryState && typeof command.setActive === 'function') {
|
3320
|
+
command.setActive();
|
3321
|
+
}
|
3322
|
+
return;
|
3323
|
+
}
|
3324
|
+
}
|
3325
|
+
// We can't use queryCommandState for this command, so add to manualStateChecks
|
3326
|
+
manualStateChecks.push(command);
|
3327
|
+
});
|
3328
|
+
|
3329
|
+
// Climb up the DOM and do manual checks for whether a certain command is currently enabled for this node
|
3330
|
+
while (parentNode.tagName !== undefined && Util.parentElements.indexOf(parentNode.tagName.toLowerCase) === -1) {
|
3331
|
+
manualStateChecks.forEach(updateExtensionState);
|
3332
|
+
|
3333
|
+
// we can abort the search upwards if we leave the contentEditable element
|
3334
|
+
if (this.base.elements.indexOf(parentNode) !== -1) {
|
3335
|
+
break;
|
3336
|
+
}
|
3337
|
+
parentNode = parentNode.parentNode;
|
3338
|
+
}
|
3339
|
+
},
|
3340
|
+
|
3341
|
+
// Positioning toolbar
|
3342
|
+
|
3343
|
+
positionToolbarIfShown: function () {
|
3344
|
+
if (this.isDisplayed()) {
|
3345
|
+
this.setToolbarPosition();
|
3346
|
+
}
|
3347
|
+
},
|
3348
|
+
|
3349
|
+
setToolbarPosition: function () {
|
3350
|
+
var container = this.getFocusedElement(),
|
3351
|
+
selection = this.options.contentWindow.getSelection(),
|
3352
|
+
anchorPreview;
|
3353
|
+
|
3354
|
+
// If there isn't a valid selection, bail
|
3355
|
+
if (!container) {
|
3356
|
+
return this;
|
3357
|
+
}
|
3358
|
+
|
3359
|
+
if (this.options.staticToolbar) {
|
3360
|
+
this.showToolbar();
|
3361
|
+
this.positionStaticToolbar(container);
|
3362
|
+
|
3363
|
+
} else if (!selection.isCollapsed) {
|
3364
|
+
this.showToolbar();
|
3365
|
+
this.positionToolbar(selection);
|
3366
|
+
}
|
3367
|
+
|
3368
|
+
anchorPreview = this.base.getExtensionByName('anchor-preview');
|
3369
|
+
|
3370
|
+
if (anchorPreview && typeof anchorPreview.hidePreview === 'function') {
|
3371
|
+
anchorPreview.hidePreview();
|
3372
|
+
}
|
3373
|
+
},
|
3374
|
+
|
3375
|
+
positionStaticToolbar: function (container) {
|
3376
|
+
// position the toolbar at left 0, so we can get the real width of the toolbar
|
3377
|
+
this.getToolbarElement().style.left = '0';
|
3378
|
+
|
3379
|
+
// document.documentElement for IE 9
|
3380
|
+
var scrollTop = (this.options.ownerDocument.documentElement && this.options.ownerDocument.documentElement.scrollTop) || this.options.ownerDocument.body.scrollTop,
|
3381
|
+
windowWidth = this.options.contentWindow.innerWidth,
|
3382
|
+
toolbarElement = this.getToolbarElement(),
|
3383
|
+
containerRect = container.getBoundingClientRect(),
|
3384
|
+
containerTop = containerRect.top + scrollTop,
|
3385
|
+
containerCenter = (containerRect.left + (containerRect.width / 2)),
|
3386
|
+
toolbarHeight = toolbarElement.offsetHeight,
|
3387
|
+
toolbarWidth = toolbarElement.offsetWidth,
|
3388
|
+
halfOffsetWidth = toolbarWidth / 2,
|
3389
|
+
targetLeft;
|
3390
|
+
|
3391
|
+
if (this.options.stickyToolbar) {
|
3392
|
+
// If it's beyond the height of the editor, position it at the bottom of the editor
|
3393
|
+
if (scrollTop > (containerTop + container.offsetHeight - toolbarHeight)) {
|
3394
|
+
toolbarElement.style.top = (containerTop + container.offsetHeight - toolbarHeight) + 'px';
|
3395
|
+
toolbarElement.classList.remove('sticky-toolbar');
|
3396
|
+
|
3397
|
+
// Stick the toolbar to the top of the window
|
3398
|
+
} else if (scrollTop > (containerTop - toolbarHeight)) {
|
3399
|
+
toolbarElement.classList.add('sticky-toolbar');
|
3400
|
+
toolbarElement.style.top = "0px";
|
3401
|
+
|
3402
|
+
// Normal static toolbar position
|
3403
|
+
} else {
|
3404
|
+
toolbarElement.classList.remove('sticky-toolbar');
|
3405
|
+
toolbarElement.style.top = containerTop - toolbarHeight + "px";
|
3406
|
+
}
|
3407
|
+
} else {
|
3408
|
+
toolbarElement.style.top = containerTop - toolbarHeight + "px";
|
3409
|
+
}
|
3410
|
+
|
3411
|
+
if (this.options.toolbarAlign === 'left') {
|
3412
|
+
targetLeft = containerRect.left;
|
3413
|
+
} else if (this.options.toolbarAlign === 'center') {
|
3414
|
+
targetLeft = containerCenter - halfOffsetWidth;
|
3415
|
+
} else if (this.options.toolbarAlign === 'right') {
|
3416
|
+
targetLeft = containerRect.right - toolbarWidth;
|
3417
|
+
}
|
3418
|
+
|
3419
|
+
if (targetLeft < 0) {
|
3420
|
+
targetLeft = 0;
|
3421
|
+
} else if ((targetLeft + toolbarWidth) > windowWidth) {
|
3422
|
+
targetLeft = (windowWidth - Math.ceil(toolbarWidth) - 1);
|
3423
|
+
}
|
3424
|
+
|
3425
|
+
toolbarElement.style.left = targetLeft + 'px';
|
3426
|
+
},
|
3427
|
+
|
3428
|
+
positionToolbar: function (selection) {
|
3429
|
+
// position the toolbar at left 0, so we can get the real width of the toolbar
|
3430
|
+
this.getToolbarElement().style.left = '0';
|
3431
|
+
|
3432
|
+
var windowWidth = this.options.contentWindow.innerWidth,
|
3433
|
+
range = selection.getRangeAt(0),
|
3434
|
+
boundary = range.getBoundingClientRect(),
|
3435
|
+
middleBoundary = (boundary.left + boundary.right) / 2,
|
3436
|
+
toolbarElement = this.getToolbarElement(),
|
3437
|
+
toolbarHeight = toolbarElement.offsetHeight,
|
3438
|
+
toolbarWidth = toolbarElement.offsetWidth,
|
3439
|
+
halfOffsetWidth = toolbarWidth / 2,
|
3440
|
+
buttonHeight = 50,
|
3441
|
+
defaultLeft = this.options.diffLeft - halfOffsetWidth;
|
3442
|
+
|
3443
|
+
if (boundary.top < buttonHeight) {
|
3444
|
+
toolbarElement.classList.add('medium-toolbar-arrow-over');
|
3445
|
+
toolbarElement.classList.remove('medium-toolbar-arrow-under');
|
3446
|
+
toolbarElement.style.top = buttonHeight + boundary.bottom - this.options.diffTop + this.options.contentWindow.pageYOffset - toolbarHeight + 'px';
|
3447
|
+
} else {
|
3448
|
+
toolbarElement.classList.add('medium-toolbar-arrow-under');
|
3449
|
+
toolbarElement.classList.remove('medium-toolbar-arrow-over');
|
3450
|
+
toolbarElement.style.top = boundary.top + this.options.diffTop + this.options.contentWindow.pageYOffset - toolbarHeight + 'px';
|
3451
|
+
}
|
3452
|
+
if (middleBoundary < halfOffsetWidth) {
|
3453
|
+
toolbarElement.style.left = defaultLeft + halfOffsetWidth + 'px';
|
3454
|
+
} else if ((windowWidth - middleBoundary) < halfOffsetWidth) {
|
3455
|
+
toolbarElement.style.left = windowWidth + defaultLeft - halfOffsetWidth + 'px';
|
3456
|
+
} else {
|
3457
|
+
toolbarElement.style.left = defaultLeft + middleBoundary + 'px';
|
3458
|
+
}
|
3459
|
+
}
|
3460
|
+
};
|
3461
|
+
}());
|
3462
|
+
|
3463
|
+
var Placeholders;
|
3464
|
+
|
3465
|
+
(function () {
|
3466
|
+
'use strict';
|
3467
|
+
|
3468
|
+
Placeholders = function (instance) {
|
3469
|
+
this.base = instance;
|
3470
|
+
|
3471
|
+
this.initPlaceholders();
|
3472
|
+
this.attachEventHandlers();
|
3473
|
+
};
|
3474
|
+
|
3475
|
+
Placeholders.prototype = {
|
3476
|
+
|
3477
|
+
initPlaceholders: function () {
|
3478
|
+
this.base.elements.forEach(function (el) {
|
3479
|
+
this.updatePlaceholder(el);
|
3480
|
+
}, this);
|
3481
|
+
},
|
3482
|
+
|
3483
|
+
showPlaceholder: function (el) {
|
3484
|
+
if (el) {
|
3485
|
+
el.classList.add('medium-editor-placeholder');
|
3486
|
+
}
|
3487
|
+
},
|
3488
|
+
|
3489
|
+
hidePlaceholder: function (el) {
|
3490
|
+
if (el) {
|
3491
|
+
el.classList.remove('medium-editor-placeholder');
|
3492
|
+
}
|
3493
|
+
},
|
3494
|
+
|
3495
|
+
updatePlaceholder: function (el) {
|
3496
|
+
if (!(el.querySelector('img')) &&
|
3497
|
+
!(el.querySelector('blockquote')) &&
|
3498
|
+
el.textContent.replace(/^\s+|\s+$/g, '') === '') {
|
3499
|
+
this.showPlaceholder(el);
|
3500
|
+
} else {
|
3501
|
+
this.hidePlaceholder(el);
|
3502
|
+
}
|
3503
|
+
},
|
3504
|
+
|
3505
|
+
attachEventHandlers: function () {
|
3506
|
+
// Custom events
|
3507
|
+
this.base.subscribe('blur', this.handleExternalInteraction.bind(this));
|
3508
|
+
|
3509
|
+
// Check placeholder on blur
|
3510
|
+
this.base.subscribe('editableBlur', this.handleBlur.bind(this));
|
3511
|
+
|
3512
|
+
// Events where we always hide the placeholder
|
3513
|
+
this.base.subscribe('editableClick', this.handleHidePlaceholderEvent.bind(this));
|
3514
|
+
this.base.subscribe('editableKeypress', this.handleHidePlaceholderEvent.bind(this));
|
3515
|
+
this.base.subscribe('editablePaste', this.handleHidePlaceholderEvent.bind(this));
|
3516
|
+
},
|
3517
|
+
|
3518
|
+
handleHidePlaceholderEvent: function (event, element) {
|
3519
|
+
// Events where we hide the placeholder
|
3520
|
+
this.hidePlaceholder(element);
|
3521
|
+
},
|
3522
|
+
|
3523
|
+
handleBlur: function (event, element) {
|
3524
|
+
// Update placeholder for element that lost focus
|
3525
|
+
this.updatePlaceholder(element);
|
3526
|
+
},
|
3527
|
+
|
3528
|
+
handleExternalInteraction: function () {
|
3529
|
+
// Update all placeholders
|
3530
|
+
this.initPlaceholders();
|
3531
|
+
}
|
3532
|
+
};
|
3533
|
+
|
3534
|
+
}());
|
3535
|
+
|
3536
|
+
var extensionDefaults;
|
3537
|
+
(function(){
|
3538
|
+
|
3539
|
+
// for now this is empty because nothing interally uses an Extension default.
|
3540
|
+
// as they are converted, provide them here.
|
3541
|
+
extensionDefaults = {
|
3542
|
+
paste: PasteHandler
|
3543
|
+
};
|
3544
|
+
|
3545
|
+
})();
|
3546
|
+
function MediumEditor(elements, options) {
|
3547
|
+
'use strict';
|
3548
|
+
return this.init(elements, options);
|
3549
|
+
}
|
3550
|
+
|
3551
|
+
(function () {
|
3552
|
+
'use strict';
|
3553
|
+
|
3554
|
+
// Event handlers that shouldn't be exposed externally
|
3555
|
+
|
3556
|
+
function handleDisabledEnterKeydown(event, element) {
|
3557
|
+
if (this.options.disableReturn || element.getAttribute('data-disable-return')) {
|
3558
|
+
event.preventDefault();
|
3559
|
+
} else if (this.options.disableDoubleReturn || this.getAttribute('data-disable-double-return')) {
|
3560
|
+
var node = Util.getSelectionStart(this.options.ownerDocument);
|
3561
|
+
if (node && node.textContent.trim() === '') {
|
3562
|
+
event.preventDefault();
|
3563
|
+
}
|
3564
|
+
}
|
3565
|
+
}
|
3566
|
+
|
3567
|
+
function handleTabKeydown(event) {
|
3568
|
+
// Override tab only for pre nodes
|
3569
|
+
var node = Util.getSelectionStart(this.options.ownerDocument),
|
3570
|
+
tag = node && node.tagName.toLowerCase();
|
3571
|
+
|
3572
|
+
if (tag === 'pre') {
|
3573
|
+
event.preventDefault();
|
3574
|
+
Util.insertHTMLCommand(this.options.ownerDocument, ' ');
|
3575
|
+
}
|
3576
|
+
|
3577
|
+
// Tab to indent list structures!
|
3578
|
+
if (Util.isListItem(node)) {
|
3579
|
+
event.preventDefault();
|
3580
|
+
|
3581
|
+
// If Shift is down, outdent, otherwise indent
|
3582
|
+
if (event.shiftKey) {
|
3583
|
+
this.options.ownerDocument.execCommand('outdent', false, null);
|
3584
|
+
} else {
|
3585
|
+
this.options.ownerDocument.execCommand('indent', false, null);
|
3586
|
+
}
|
3587
|
+
}
|
3588
|
+
}
|
3589
|
+
|
3590
|
+
function handleBlockDeleteKeydowns(event) {
|
3591
|
+
var range, sel, p, node = Util.getSelectionStart(this.options.ownerDocument),
|
3592
|
+
tagName = node.tagName.toLowerCase(),
|
3593
|
+
isEmpty = /^(\s+|<br\/?>)?$/i,
|
3594
|
+
isHeader = /h\d/i;
|
3595
|
+
|
3596
|
+
if ((event.which === Util.keyCode.BACKSPACE || event.which === Util.keyCode.ENTER) &&
|
3597
|
+
// has a preceeding sibling
|
3598
|
+
node.previousElementSibling &&
|
3599
|
+
// in a header
|
3600
|
+
isHeader.test(tagName) &&
|
3601
|
+
// at the very end of the block
|
3602
|
+
Selection.getCaretOffsets(node).left === 0) {
|
3603
|
+
if (event.which === Util.keyCode.BACKSPACE && isEmpty.test(node.previousElementSibling.innerHTML)) {
|
3604
|
+
// backspacing the begining of a header into an empty previous element will
|
3605
|
+
// change the tagName of the current node to prevent one
|
3606
|
+
// instead delete previous node and cancel the event.
|
3607
|
+
node.previousElementSibling.parentNode.removeChild(node.previousElementSibling);
|
3608
|
+
event.preventDefault();
|
3609
|
+
} else if (event.which === Util.keyCode.ENTER) {
|
3610
|
+
// hitting return in the begining of a header will create empty header elements before the current one
|
3611
|
+
// instead, make "<p><br></p>" element, which are what happens if you hit return in an empty paragraph
|
3612
|
+
p = this.options.ownerDocument.createElement('p');
|
3613
|
+
p.innerHTML = '<br>';
|
3614
|
+
node.previousElementSibling.parentNode.insertBefore(p, node);
|
3615
|
+
event.preventDefault();
|
3616
|
+
}
|
3617
|
+
} else if (event.which === Util.keyCode.DELETE &&
|
3618
|
+
// between two sibling elements
|
3619
|
+
node.nextElementSibling &&
|
3620
|
+
node.previousElementSibling &&
|
3621
|
+
// not in a header
|
3622
|
+
!isHeader.test(tagName) &&
|
3623
|
+
// in an empty tag
|
3624
|
+
isEmpty.test(node.innerHTML) &&
|
3625
|
+
// when the next tag *is* a header
|
3626
|
+
isHeader.test(node.nextElementSibling.tagName)) {
|
3627
|
+
// hitting delete in an empty element preceding a header, ex:
|
3628
|
+
// <p>[CURSOR]</p><h1>Header</h1>
|
3629
|
+
// Will cause the h1 to become a paragraph.
|
3630
|
+
// Instead, delete the paragraph node and move the cursor to the begining of the h1
|
3631
|
+
|
3632
|
+
// remove node and move cursor to start of header
|
3633
|
+
range = this.options.ownerDocument.createRange();
|
3634
|
+
sel = this.options.ownerDocument.getSelection();
|
3635
|
+
|
3636
|
+
range.setStart(node.nextElementSibling, 0);
|
3637
|
+
range.collapse(true);
|
3638
|
+
|
3639
|
+
sel.removeAllRanges();
|
3640
|
+
sel.addRange(range);
|
3641
|
+
|
3642
|
+
node.previousElementSibling.parentNode.removeChild(node);
|
3643
|
+
|
3644
|
+
event.preventDefault();
|
3645
|
+
} else if (event.which === Util.keyCode.BACKSPACE &&
|
3646
|
+
tagName === 'li' &&
|
3647
|
+
// hitting backspace inside an empty li
|
3648
|
+
isEmpty.test(node.innerHTML) &&
|
3649
|
+
// is first element (no preceeding siblings)
|
3650
|
+
!node.previousElementSibling &&
|
3651
|
+
// parent also does not have a sibling
|
3652
|
+
!node.parentElement.previousElementSibling &&
|
3653
|
+
// is not the only li in a list
|
3654
|
+
node.nextElementSibling.tagName.toLowerCase() === 'li') {
|
3655
|
+
// backspacing in an empty first list element in the first list (with more elements) ex:
|
3656
|
+
// <ul><li>[CURSOR]</li><li>List Item 2</li></ul>
|
3657
|
+
// will remove the first <li> but add some extra element before (varies based on browser)
|
3658
|
+
// Instead, this will:
|
3659
|
+
// 1) remove the list element
|
3660
|
+
// 2) create a paragraph before the list
|
3661
|
+
// 3) move the cursor into the paragraph
|
3662
|
+
|
3663
|
+
// create a paragraph before the list
|
3664
|
+
p = this.options.ownerDocument.createElement('p');
|
3665
|
+
p.innerHTML = '<br>';
|
3666
|
+
node.parentElement.parentElement.insertBefore(p, node.parentElement);
|
3667
|
+
|
3668
|
+
// move the cursor into the new paragraph
|
3669
|
+
range = this.options.ownerDocument.createRange();
|
3670
|
+
sel = this.options.ownerDocument.getSelection();
|
3671
|
+
range.setStart(p, 0);
|
3672
|
+
range.collapse(true);
|
3673
|
+
sel.removeAllRanges();
|
3674
|
+
sel.addRange(range);
|
3675
|
+
|
3676
|
+
// remove the list element
|
3677
|
+
node.parentElement.removeChild(node);
|
3678
|
+
|
3679
|
+
event.preventDefault();
|
3680
|
+
}
|
3681
|
+
}
|
3682
|
+
|
3683
|
+
function handleDrag(event) {
|
3684
|
+
var className = 'medium-editor-dragover';
|
3685
|
+
event.preventDefault();
|
3686
|
+
event.dataTransfer.dropEffect = 'copy';
|
3687
|
+
|
3688
|
+
if (event.type === 'dragover') {
|
3689
|
+
event.target.classList.add(className);
|
3690
|
+
} else if (event.type === 'dragleave') {
|
3691
|
+
event.target.classList.remove(className);
|
3692
|
+
}
|
3693
|
+
}
|
3694
|
+
|
3695
|
+
function handleDrop(event) {
|
3696
|
+
var className = 'medium-editor-dragover',
|
3697
|
+
files;
|
3698
|
+
event.preventDefault();
|
3699
|
+
event.stopPropagation();
|
3700
|
+
|
3701
|
+
// IE9 does not support the File API, so prevent file from opening in a new window
|
3702
|
+
// but also don't try to actually get the file
|
3703
|
+
if (event.dataTransfer.files) {
|
3704
|
+
files = Array.prototype.slice.call(event.dataTransfer.files, 0);
|
3705
|
+
files.some(function (file) {
|
3706
|
+
if (file.type.match("image")) {
|
3707
|
+
var fileReader, id;
|
3708
|
+
fileReader = new FileReader();
|
3709
|
+
fileReader.readAsDataURL(file);
|
3710
|
+
|
3711
|
+
id = 'medium-img-' + (+new Date());
|
3712
|
+
Util.insertHTMLCommand(this.options.ownerDocument, '<img class="medium-image-loading" id="' + id + '" />');
|
3713
|
+
|
3714
|
+
fileReader.onload = function () {
|
3715
|
+
var img = this.options.ownerDocument.getElementById(id);
|
3716
|
+
if (img) {
|
3717
|
+
img.removeAttribute('id');
|
3718
|
+
img.removeAttribute('class');
|
3719
|
+
img.src = fileReader.result;
|
3720
|
+
}
|
3721
|
+
}.bind(this);
|
3722
|
+
}
|
3723
|
+
}.bind(this));
|
3724
|
+
}
|
3725
|
+
event.target.classList.remove(className);
|
3726
|
+
}
|
3727
|
+
|
3728
|
+
function handleKeyup(event) {
|
3729
|
+
var node = Util.getSelectionStart(this.options.ownerDocument),
|
3730
|
+
tagName;
|
3731
|
+
|
3732
|
+
if (!node) {
|
3733
|
+
return;
|
3734
|
+
}
|
3735
|
+
|
3736
|
+
if (node.getAttribute('data-medium-element') && node.children.length === 0) {
|
3737
|
+
this.options.ownerDocument.execCommand('formatBlock', false, 'p');
|
3738
|
+
}
|
3739
|
+
|
3740
|
+
if (event.which === Util.keyCode.ENTER && !Util.isListItem(node)) {
|
3741
|
+
tagName = node.tagName.toLowerCase();
|
3742
|
+
// For anchor tags, unlink
|
3743
|
+
if (tagName === 'a') {
|
3744
|
+
this.options.ownerDocument.execCommand('unlink', false, null);
|
3745
|
+
} else if (!event.shiftKey) {
|
3746
|
+
// only format block if this is not a header tag
|
3747
|
+
if (!/h\d/.test(tagName)) {
|
3748
|
+
this.options.ownerDocument.execCommand('formatBlock', false, 'p');
|
3749
|
+
}
|
3750
|
+
}
|
3751
|
+
}
|
3752
|
+
}
|
3753
|
+
|
3754
|
+
// Internal helper methods which shouldn't be exposed externally
|
3755
|
+
|
3756
|
+
function createElementsArray(selector) {
|
3757
|
+
if (!selector) {
|
3758
|
+
selector = [];
|
3759
|
+
}
|
3760
|
+
// If string, use as query selector
|
3761
|
+
if (typeof selector === 'string') {
|
3762
|
+
selector = this.options.ownerDocument.querySelectorAll(selector);
|
3763
|
+
}
|
3764
|
+
// If element, put into array
|
3765
|
+
if (Util.isElement(selector)) {
|
3766
|
+
selector = [selector];
|
3767
|
+
}
|
3768
|
+
// Convert NodeList (or other array like object) into an array
|
3769
|
+
this.elements = Array.prototype.slice.apply(selector);
|
3770
|
+
}
|
3771
|
+
|
3772
|
+
function initExtension(extension, name, instance) {
|
3773
|
+
if (extension.parent) {
|
3774
|
+
extension.base = instance;
|
3775
|
+
}
|
3776
|
+
if (typeof extension.init === 'function') {
|
3777
|
+
extension.init(instance);
|
3778
|
+
}
|
3779
|
+
if (!extension.name) {
|
3780
|
+
extension.name = name;
|
3781
|
+
}
|
3782
|
+
return extension;
|
3783
|
+
}
|
3784
|
+
|
3785
|
+
function shouldAddDefaultAnchorPreview() {
|
3786
|
+
var i,
|
3787
|
+
shouldAdd = false;
|
3788
|
+
|
3789
|
+
// If anchor-preview is disabled, don't add
|
3790
|
+
if (this.options.disableAnchorPreview) {
|
3791
|
+
return false;
|
3792
|
+
}
|
3793
|
+
// If anchor-preview extension has been overriden, don't add
|
3794
|
+
if (this.options.extensions['anchor-preview']) {
|
3795
|
+
return false;
|
3796
|
+
}
|
3797
|
+
// If toolbar is disabled, don't add
|
3798
|
+
if (this.options.disableToolbar) {
|
3799
|
+
return false;
|
3800
|
+
}
|
3801
|
+
// If all elements have 'data-disable-toolbar' attribute, don't add
|
3802
|
+
for (i = 0; i < this.elements.length; i += 1) {
|
3803
|
+
if (!this.elements[i].getAttribute('data-disable-toolbar')) {
|
3804
|
+
shouldAdd = true;
|
3805
|
+
break;
|
3806
|
+
}
|
3807
|
+
}
|
3808
|
+
|
3809
|
+
return shouldAdd;
|
3810
|
+
}
|
3811
|
+
|
3812
|
+
function createContentEditable(index) {
|
3813
|
+
var div = this.options.ownerDocument.createElement('div');
|
3814
|
+
var id = (+new Date());
|
3815
|
+
var textarea = this.elements[index];
|
3816
|
+
|
3817
|
+
div.className = textarea.className;
|
3818
|
+
div.id = id;
|
3819
|
+
div.innerHTML = textarea.value;
|
3820
|
+
|
3821
|
+
textarea.classList.add('medium-editor-hidden');
|
3822
|
+
textarea.parentNode.insertBefore(
|
3823
|
+
div,
|
3824
|
+
textarea
|
3825
|
+
);
|
3826
|
+
|
3827
|
+
this.on(div, 'input', function () {
|
3828
|
+
textarea.value = this.serialize()[id].value;
|
3829
|
+
}.bind(this));
|
3830
|
+
return div;
|
3831
|
+
}
|
3832
|
+
|
3833
|
+
function initElements() {
|
3834
|
+
var i,
|
3835
|
+
addToolbar = false;
|
3836
|
+
for (i = 0; i < this.elements.length; i += 1) {
|
3837
|
+
if (!this.options.disableEditing && !this.elements[i].getAttribute('data-disable-editing')) {
|
3838
|
+
if (this.elements[i].tagName.toLowerCase() === 'textarea') {
|
3839
|
+
this.elements[i] = createContentEditable.call(this, i);
|
3840
|
+
}
|
3841
|
+
this.elements[i].setAttribute('contentEditable', true);
|
3842
|
+
this.elements[i].setAttribute('spellcheck', this.options.spellcheck);
|
3843
|
+
}
|
3844
|
+
if (!this.elements[i].getAttribute('data-placeholder')) {
|
3845
|
+
this.elements[i].setAttribute('data-placeholder', this.options.placeholder);
|
3846
|
+
}
|
3847
|
+
this.elements[i].setAttribute('data-medium-element', true);
|
3848
|
+
this.elements[i].setAttribute('role', 'textbox');
|
3849
|
+
this.elements[i].setAttribute('aria-multiline', true);
|
3850
|
+
if (!this.options.disableToolbar && !this.elements[i].getAttribute('data-disable-toolbar')) {
|
3851
|
+
addToolbar = true;
|
3852
|
+
}
|
3853
|
+
}
|
3854
|
+
// Init toolbar
|
3855
|
+
if (!this.toolbar && addToolbar) {
|
3856
|
+
this.toolbar = new Toolbar(this);
|
3857
|
+
this.options.elementsContainer.appendChild(this.toolbar.getToolbarElement());
|
3858
|
+
}
|
3859
|
+
}
|
3860
|
+
|
3861
|
+
function attachHandlers() {
|
3862
|
+
var i;
|
3863
|
+
|
3864
|
+
// attach to tabs
|
3865
|
+
this.subscribe('editableKeydownTab', handleTabKeydown.bind(this));
|
3866
|
+
|
3867
|
+
// Bind keys which can create or destroy a block element: backspace, delete, return
|
3868
|
+
this.subscribe('editableKeydownDelete', handleBlockDeleteKeydowns.bind(this));
|
3869
|
+
this.subscribe('editableKeydownEnter', handleBlockDeleteKeydowns.bind(this));
|
3870
|
+
|
3871
|
+
// disabling return or double return
|
3872
|
+
if (this.options.disableReturn || this.options.disableDoubleReturn) {
|
3873
|
+
this.subscribe('editableKeydownEnter', handleDisabledEnterKeydown.bind(this));
|
3874
|
+
} else {
|
3875
|
+
for (i = 0; i < this.elements.length; i += 1) {
|
3876
|
+
if (this.elements[i].getAttribute('data-disable-return') || this.elements[i].getAttribute('data-disable-double-return')) {
|
3877
|
+
this.subscribe('editableKeydownEnter', handleDisabledEnterKeydown.bind(this));
|
3878
|
+
break;
|
3879
|
+
}
|
3880
|
+
}
|
3881
|
+
}
|
3882
|
+
|
3883
|
+
// if we're not disabling return, add a handler to help handle cleanup
|
3884
|
+
// for certain cases when enter is pressed
|
3885
|
+
if (!this.options.disableReturn) {
|
3886
|
+
this.elements.forEach(function (element) {
|
3887
|
+
if (!element.getAttribute('data-disable-return')) {
|
3888
|
+
this.on(element, 'keyup', handleKeyup.bind(this));
|
3889
|
+
}
|
3890
|
+
}, this);
|
3891
|
+
}
|
3892
|
+
|
3893
|
+
// drag and drop of images
|
3894
|
+
if (this.options.imageDragging) {
|
3895
|
+
this.subscribe('editableDrag', handleDrag.bind(this));
|
3896
|
+
this.subscribe('editableDrop', handleDrop.bind(this));
|
3897
|
+
}
|
3898
|
+
}
|
3899
|
+
|
3900
|
+
function initPasteHandler(options) {
|
3901
|
+
// Backwards compatability
|
3902
|
+
var defaultsBC = {
|
3903
|
+
forcePlainText: this.options.forcePlainText, // deprecated
|
3904
|
+
cleanPastedHTML: this.options.cleanPastedHTML, // deprecated
|
3905
|
+
disableReturn: this.options.disableReturn,
|
3906
|
+
targetBlank: this.options.targetBlank,
|
3907
|
+
"window": this.options.contentWindow,
|
3908
|
+
"document": this.options.ownerDocument
|
3909
|
+
};
|
3910
|
+
|
3911
|
+
return new MediumEditor.extensions.paste(
|
3912
|
+
Util.extend({}, options, defaultsBC)
|
3913
|
+
);
|
3914
|
+
}
|
3915
|
+
|
3916
|
+
function initCommands() {
|
3917
|
+
var buttons = this.options.buttons,
|
3918
|
+
extensions = this.options.extensions,
|
3919
|
+
ext,
|
3920
|
+
name;
|
3921
|
+
this.commands = [];
|
3922
|
+
|
3923
|
+
buttons.forEach(function (buttonName) {
|
3924
|
+
if (extensions[buttonName]) {
|
3925
|
+
ext = initExtension(extensions[buttonName], buttonName, this);
|
3926
|
+
this.commands.push(ext);
|
3927
|
+
} else if (buttonName === 'anchor') {
|
3928
|
+
ext = initExtension(new AnchorExtension(), buttonName, this);
|
3929
|
+
this.commands.push(ext);
|
3930
|
+
} else if (buttonName === 'fontsize') {
|
3931
|
+
ext = initExtension(new FontSizeExtension(), buttonName, this);
|
3932
|
+
this.commands.push(ext);
|
3933
|
+
} else if (ButtonsData.hasOwnProperty(buttonName)) {
|
3934
|
+
ext = new DefaultButton(ButtonsData[buttonName], this);
|
3935
|
+
this.commands.push(ext);
|
3936
|
+
}
|
3937
|
+
}, this);
|
3938
|
+
|
3939
|
+
for (name in extensions) {
|
3940
|
+
if (extensions.hasOwnProperty(name) && buttons.indexOf(name) === -1) {
|
3941
|
+
ext = initExtension(extensions[name], name, this);
|
3942
|
+
this.commands.push(ext);
|
3943
|
+
}
|
3944
|
+
}
|
3945
|
+
|
3946
|
+
// Only add default paste extension if it wasn't overriden
|
3947
|
+
if (!this.options.extensions['paste']) {
|
3948
|
+
this.commands.push(initExtension(initPasteHandler.call(this, this.options.paste), 'paste', this));
|
3949
|
+
}
|
3950
|
+
|
3951
|
+
// Add AnchorPreview as extension if needed
|
3952
|
+
if (shouldAddDefaultAnchorPreview.call(this)) {
|
3953
|
+
this.commands.push(initExtension(new AnchorPreview(), 'anchor-preview', this));
|
3954
|
+
}
|
3955
|
+
}
|
3956
|
+
|
3957
|
+
function mergeOptions(defaults, options) {
|
3958
|
+
// warn about using deprecated properties
|
3959
|
+
if (options) {
|
3960
|
+
[['forcePlainText', 'paste.forcePlainText'],
|
3961
|
+
['cleanPastedHTML', 'paste.cleanPastedHTML']].forEach(function (pair) {
|
3962
|
+
if (options.hasOwnProperty(pair[0]) && options[pair[0]] !== undefined) {
|
3963
|
+
Util.deprecated(pair[0], pair[1], 'v5.0.0');
|
3964
|
+
}
|
3965
|
+
});
|
3966
|
+
}
|
3967
|
+
|
3968
|
+
var nestedMerges = ['paste'];
|
3969
|
+
var tempOpts = Util.extend({}, options);
|
3970
|
+
|
3971
|
+
nestedMerges.forEach(function (toMerge) {
|
3972
|
+
if (!tempOpts[toMerge]) {
|
3973
|
+
tempOpts[toMerge] = defaults[toMerge];
|
3974
|
+
} else {
|
3975
|
+
tempOpts[toMerge] = Util.defaults({}, tempOpts[toMerge], defaults[toMerge]);
|
3976
|
+
}
|
3977
|
+
});
|
3978
|
+
|
3979
|
+
return Util.defaults(tempOpts, defaults);
|
3980
|
+
}
|
3981
|
+
|
3982
|
+
function execActionInternal(action, opts) {
|
3983
|
+
/*jslint regexp: true*/
|
3984
|
+
var appendAction = /^append-(.+)$/gi,
|
3985
|
+
match;
|
3986
|
+
/*jslint regexp: false*/
|
3987
|
+
|
3988
|
+
// Actions starting with 'append-' should attempt to format a block of text ('formatBlock') using a specific
|
3989
|
+
// type of block element (ie append-blockquote, append-h1, append-pre, etc.)
|
3990
|
+
match = appendAction.exec(action);
|
3991
|
+
if (match) {
|
3992
|
+
return Util.execFormatBlock(this.options.ownerDocument, match[1]);
|
3993
|
+
}
|
3994
|
+
|
3995
|
+
if (action === 'fontSize') {
|
3996
|
+
return this.options.ownerDocument.execCommand('fontSize', false, opts.size);
|
3997
|
+
}
|
3998
|
+
|
3999
|
+
if (action === 'createLink') {
|
4000
|
+
return this.createLink(opts);
|
4001
|
+
}
|
4002
|
+
|
4003
|
+
if (action === 'image') {
|
4004
|
+
return this.options.ownerDocument.execCommand('insertImage', false, this.options.contentWindow.getSelection());
|
4005
|
+
}
|
4006
|
+
|
4007
|
+
return this.options.ownerDocument.execCommand(action, false, null);
|
4008
|
+
}
|
4009
|
+
|
4010
|
+
// deprecate
|
4011
|
+
MediumEditor.statics = {
|
4012
|
+
ButtonsData: ButtonsData,
|
4013
|
+
DefaultButton: DefaultButton,
|
4014
|
+
AnchorExtension: AnchorExtension,
|
4015
|
+
FontSizeExtension: FontSizeExtension,
|
4016
|
+
Toolbar: Toolbar,
|
4017
|
+
AnchorPreview: AnchorPreview
|
4018
|
+
};
|
4019
|
+
|
4020
|
+
MediumEditor.Extension = Extension;
|
4021
|
+
|
4022
|
+
MediumEditor.extensions = extensionDefaults;
|
4023
|
+
MediumEditor.util = Util;
|
4024
|
+
MediumEditor.selection = Selection;
|
4025
|
+
|
4026
|
+
MediumEditor.prototype = {
|
4027
|
+
|
4028
|
+
defaults: editorDefaults,
|
4029
|
+
|
4030
|
+
// NOT DOCUMENTED - exposed for backwards compatability
|
4031
|
+
init: function (elements, options) {
|
4032
|
+
var uniqueId = 1;
|
4033
|
+
|
4034
|
+
this.options = mergeOptions.call(this, this.defaults, options);
|
4035
|
+
createElementsArray.call(this, elements);
|
4036
|
+
if (this.elements.length === 0) {
|
4037
|
+
return;
|
4038
|
+
}
|
4039
|
+
|
4040
|
+
if (!this.options.elementsContainer) {
|
4041
|
+
this.options.elementsContainer = this.options.ownerDocument.body;
|
4042
|
+
}
|
4043
|
+
|
4044
|
+
while (this.options.elementsContainer.querySelector('#medium-editor-toolbar-' + uniqueId)) {
|
4045
|
+
uniqueId = uniqueId + 1;
|
4046
|
+
}
|
4047
|
+
|
4048
|
+
this.id = uniqueId;
|
4049
|
+
|
4050
|
+
return this.setup();
|
4051
|
+
},
|
4052
|
+
|
4053
|
+
setup: function () {
|
4054
|
+
if (this.isActive) {
|
4055
|
+
return;
|
4056
|
+
}
|
4057
|
+
|
4058
|
+
this.events = new Events(this);
|
4059
|
+
this.isActive = true;
|
4060
|
+
|
4061
|
+
// Call initialization helpers
|
4062
|
+
initCommands.call(this);
|
4063
|
+
initElements.call(this);
|
4064
|
+
attachHandlers.call(this);
|
4065
|
+
|
4066
|
+
if (!this.options.disablePlaceholders) {
|
4067
|
+
this.placeholders = new Placeholders(this);
|
4068
|
+
}
|
4069
|
+
},
|
4070
|
+
|
4071
|
+
destroy: function () {
|
4072
|
+
if (!this.isActive) {
|
4073
|
+
return;
|
4074
|
+
}
|
4075
|
+
|
4076
|
+
var i;
|
4077
|
+
|
4078
|
+
this.isActive = false;
|
4079
|
+
|
4080
|
+
if (this.toolbar !== undefined) {
|
4081
|
+
this.toolbar.deactivate();
|
4082
|
+
delete this.toolbar;
|
4083
|
+
}
|
4084
|
+
|
4085
|
+
for (i = 0; i < this.elements.length; i += 1) {
|
4086
|
+
this.elements[i].removeAttribute('contentEditable');
|
4087
|
+
this.elements[i].removeAttribute('spellcheck');
|
4088
|
+
this.elements[i].removeAttribute('data-medium-element');
|
4089
|
+
}
|
4090
|
+
|
4091
|
+
this.commands.forEach(function (extension) {
|
4092
|
+
if (typeof extension.deactivate === 'function') {
|
4093
|
+
extension.deactivate();
|
4094
|
+
}
|
4095
|
+
}, this);
|
4096
|
+
|
4097
|
+
this.events.detachAllDOMEvents();
|
4098
|
+
this.events.detachAllCustomEvents();
|
4099
|
+
},
|
4100
|
+
|
4101
|
+
on: function (target, event, listener, useCapture) {
|
4102
|
+
this.events.attachDOMEvent(target, event, listener, useCapture);
|
4103
|
+
},
|
4104
|
+
|
4105
|
+
off: function (target, event, listener, useCapture) {
|
4106
|
+
this.events.detachDOMEvent(target, event, listener, useCapture);
|
4107
|
+
},
|
4108
|
+
|
4109
|
+
subscribe: function (event, listener) {
|
4110
|
+
this.events.attachCustomEvent(event, listener);
|
4111
|
+
},
|
4112
|
+
|
4113
|
+
unsubscribe: function (event, listener) {
|
4114
|
+
this.events.detachCustomEvent(event, listener);
|
4115
|
+
},
|
4116
|
+
|
4117
|
+
delay: function (fn) {
|
4118
|
+
var self = this;
|
4119
|
+
return setTimeout(function () {
|
4120
|
+
if (self.isActive) {
|
4121
|
+
fn();
|
4122
|
+
}
|
4123
|
+
}, this.options.delay);
|
4124
|
+
},
|
4125
|
+
|
4126
|
+
serialize: function () {
|
4127
|
+
var i,
|
4128
|
+
elementid,
|
4129
|
+
content = {};
|
4130
|
+
for (i = 0; i < this.elements.length; i += 1) {
|
4131
|
+
elementid = (this.elements[i].id !== '') ? this.elements[i].id : 'element-' + i;
|
4132
|
+
content[elementid] = {
|
4133
|
+
value: this.elements[i].innerHTML.trim()
|
4134
|
+
};
|
4135
|
+
}
|
4136
|
+
return content;
|
4137
|
+
},
|
4138
|
+
|
4139
|
+
getExtensionByName: function (name) {
|
4140
|
+
var extension;
|
4141
|
+
if (this.commands && this.commands.length) {
|
4142
|
+
this.commands.some(function (ext) {
|
4143
|
+
if (ext.name === name) {
|
4144
|
+
extension = ext;
|
4145
|
+
return true;
|
4146
|
+
}
|
4147
|
+
return false;
|
4148
|
+
});
|
4149
|
+
}
|
4150
|
+
return extension;
|
4151
|
+
},
|
4152
|
+
|
4153
|
+
/**
|
4154
|
+
* NOT DOCUMENTED - exposed for backwards compatability
|
4155
|
+
* Helper function to call a method with a number of parameters on all registered extensions.
|
4156
|
+
* The function assures that the function exists before calling.
|
4157
|
+
*
|
4158
|
+
* @param {string} funcName name of the function to call
|
4159
|
+
* @param [args] arguments passed into funcName
|
4160
|
+
*/
|
4161
|
+
callExtensions: function (funcName) {
|
4162
|
+
if (arguments.length < 1) {
|
4163
|
+
return;
|
4164
|
+
}
|
4165
|
+
|
4166
|
+
var args = Array.prototype.slice.call(arguments, 1),
|
4167
|
+
ext,
|
4168
|
+
name;
|
4169
|
+
|
4170
|
+
for (name in this.options.extensions) {
|
4171
|
+
if (this.options.extensions.hasOwnProperty(name)) {
|
4172
|
+
ext = this.options.extensions[name];
|
4173
|
+
if (ext[funcName] !== undefined) {
|
4174
|
+
ext[funcName].apply(ext, args);
|
4175
|
+
}
|
4176
|
+
}
|
4177
|
+
}
|
4178
|
+
return this;
|
4179
|
+
},
|
4180
|
+
|
4181
|
+
stopSelectionUpdates: function () {
|
4182
|
+
this.preventSelectionUpdates = true;
|
4183
|
+
},
|
4184
|
+
|
4185
|
+
startSelectionUpdates: function () {
|
4186
|
+
this.preventSelectionUpdates = false;
|
4187
|
+
},
|
4188
|
+
|
4189
|
+
// NOT DOCUMENTED - exposed as extension helper and for backwards compatability
|
4190
|
+
checkSelection: function () {
|
4191
|
+
if (this.toolbar) {
|
4192
|
+
this.toolbar.checkState();
|
4193
|
+
}
|
4194
|
+
return this;
|
4195
|
+
},
|
4196
|
+
|
4197
|
+
// Wrapper around document.queryCommandState for checking whether an action has already
|
4198
|
+
// been applied to the current selection
|
4199
|
+
queryCommandState: function (action) {
|
4200
|
+
var fullAction = /^full-(.+)$/gi,
|
4201
|
+
match,
|
4202
|
+
queryState = null;
|
4203
|
+
|
4204
|
+
// Actions starting with 'full-' need to be modified since this is a medium-editor concept
|
4205
|
+
match = fullAction.exec(action);
|
4206
|
+
if (match) {
|
4207
|
+
action = match[1];
|
4208
|
+
}
|
4209
|
+
|
4210
|
+
try {
|
4211
|
+
queryState = this.options.ownerDocument.queryCommandState(action);
|
4212
|
+
} catch (exc) {
|
4213
|
+
queryState = null;
|
4214
|
+
}
|
4215
|
+
|
4216
|
+
return queryState;
|
4217
|
+
},
|
4218
|
+
|
4219
|
+
execAction: function (action, opts) {
|
4220
|
+
/*jslint regexp: true*/
|
4221
|
+
var fullAction = /^full-(.+)$/gi,
|
4222
|
+
match,
|
4223
|
+
result;
|
4224
|
+
/*jslint regexp: false*/
|
4225
|
+
|
4226
|
+
// Actions starting with 'full-' should be applied to to the entire contents of the editable element
|
4227
|
+
// (ie full-bold, full-append-pre, etc.)
|
4228
|
+
match = fullAction.exec(action);
|
4229
|
+
if (match) {
|
4230
|
+
// Store the current selection to be restored after applying the action
|
4231
|
+
this.saveSelection();
|
4232
|
+
// Select all of the contents before calling the action
|
4233
|
+
this.selectAllContents();
|
4234
|
+
result = execActionInternal.call(this, match[1], opts);
|
4235
|
+
// Restore the previous selection
|
4236
|
+
this.restoreSelection();
|
4237
|
+
} else {
|
4238
|
+
result = execActionInternal.call(this, action, opts);
|
4239
|
+
}
|
4240
|
+
|
4241
|
+
// do some DOM clean-up for known browser issues after the action
|
4242
|
+
if (action === 'insertunorderedlist' || action === 'insertorderedlist') {
|
4243
|
+
Util.cleanListDOM(this.getSelectedParentElement());
|
4244
|
+
}
|
4245
|
+
|
4246
|
+
this.checkSelection();
|
4247
|
+
return result;
|
4248
|
+
},
|
4249
|
+
|
4250
|
+
getSelectedParentElement: function (range) {
|
4251
|
+
if (range === undefined) {
|
4252
|
+
range = this.options.contentWindow.getSelection().getRangeAt(0);
|
4253
|
+
}
|
4254
|
+
return Selection.getSelectedParentElement(range);
|
4255
|
+
},
|
4256
|
+
|
4257
|
+
// NOT DOCUMENTED - exposed as extension helper
|
4258
|
+
hideToolbarDefaultActions: function () {
|
4259
|
+
if (this.toolbar) {
|
4260
|
+
this.toolbar.hideToolbarDefaultActions();
|
4261
|
+
}
|
4262
|
+
return this;
|
4263
|
+
},
|
4264
|
+
|
4265
|
+
// NOT DOCUMENTED - exposed as extension helper and for backwards compatability
|
4266
|
+
setToolbarPosition: function () {
|
4267
|
+
if (this.toolbar) {
|
4268
|
+
this.toolbar.setToolbarPosition();
|
4269
|
+
}
|
4270
|
+
},
|
4271
|
+
|
4272
|
+
selectAllContents: function () {
|
4273
|
+
var currNode = Selection.getSelectionElement(this.options.contentWindow);
|
4274
|
+
|
4275
|
+
if (currNode) {
|
4276
|
+
// Move to the lowest descendant node that still selects all of the contents
|
4277
|
+
while (currNode.children.length === 1) {
|
4278
|
+
currNode = currNode.children[0];
|
4279
|
+
}
|
4280
|
+
|
4281
|
+
this.selectElement(currNode);
|
4282
|
+
}
|
4283
|
+
},
|
4284
|
+
|
4285
|
+
selectElement: function (element) {
|
4286
|
+
Selection.selectNode(element, this.options.ownerDocument);
|
4287
|
+
|
4288
|
+
var selElement = Selection.getSelectionElement(this.options.contentWindow);
|
4289
|
+
if (selElement) {
|
4290
|
+
this.events.focusElement(selElement);
|
4291
|
+
}
|
4292
|
+
},
|
4293
|
+
|
4294
|
+
// http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html
|
4295
|
+
// Tim Down
|
4296
|
+
// TODO: move to selection.js and clean up old methods there
|
4297
|
+
saveSelection: function () {
|
4298
|
+
this.selectionState = null;
|
4299
|
+
|
4300
|
+
var selection = this.options.contentWindow.getSelection(),
|
4301
|
+
range,
|
4302
|
+
preSelectionRange,
|
4303
|
+
start,
|
4304
|
+
editableElementIndex = -1;
|
4305
|
+
|
4306
|
+
if (selection.rangeCount > 0) {
|
4307
|
+
range = selection.getRangeAt(0);
|
4308
|
+
preSelectionRange = range.cloneRange();
|
4309
|
+
|
4310
|
+
// Find element current selection is inside
|
4311
|
+
this.elements.some(function (el, index) {
|
4312
|
+
if (el === range.startContainer || Util.isDescendant(el, range.startContainer)) {
|
4313
|
+
editableElementIndex = index;
|
4314
|
+
return true;
|
4315
|
+
}
|
4316
|
+
return false;
|
4317
|
+
});
|
4318
|
+
|
4319
|
+
if (editableElementIndex > -1) {
|
4320
|
+
preSelectionRange.selectNodeContents(this.elements[editableElementIndex]);
|
4321
|
+
preSelectionRange.setEnd(range.startContainer, range.startOffset);
|
4322
|
+
start = preSelectionRange.toString().length;
|
4323
|
+
|
4324
|
+
this.selectionState = {
|
4325
|
+
start: start,
|
4326
|
+
end: start + range.toString().length,
|
4327
|
+
editableElementIndex: editableElementIndex
|
4328
|
+
};
|
4329
|
+
}
|
4330
|
+
}
|
4331
|
+
},
|
4332
|
+
|
4333
|
+
// http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html
|
4334
|
+
// Tim Down
|
4335
|
+
// TODO: move to selection.js and clean up old methods there
|
4336
|
+
restoreSelection: function () {
|
4337
|
+
if (!this.selectionState) {
|
4338
|
+
return;
|
4339
|
+
}
|
4340
|
+
|
4341
|
+
var editableElement = this.elements[this.selectionState.editableElementIndex],
|
4342
|
+
charIndex = 0,
|
4343
|
+
range = this.options.ownerDocument.createRange(),
|
4344
|
+
nodeStack = [editableElement],
|
4345
|
+
node,
|
4346
|
+
foundStart = false,
|
4347
|
+
stop = false,
|
4348
|
+
i,
|
4349
|
+
sel,
|
4350
|
+
nextCharIndex;
|
4351
|
+
|
4352
|
+
range.setStart(editableElement, 0);
|
4353
|
+
range.collapse(true);
|
4354
|
+
|
4355
|
+
node = nodeStack.pop();
|
4356
|
+
while (!stop && node) {
|
4357
|
+
if (node.nodeType === 3) {
|
4358
|
+
nextCharIndex = charIndex + node.length;
|
4359
|
+
if (!foundStart && this.selectionState.start >= charIndex && this.selectionState.start <= nextCharIndex) {
|
4360
|
+
range.setStart(node, this.selectionState.start - charIndex);
|
4361
|
+
foundStart = true;
|
4362
|
+
}
|
4363
|
+
if (foundStart && this.selectionState.end >= charIndex && this.selectionState.end <= nextCharIndex) {
|
4364
|
+
range.setEnd(node, this.selectionState.end - charIndex);
|
4365
|
+
stop = true;
|
4366
|
+
}
|
4367
|
+
charIndex = nextCharIndex;
|
4368
|
+
} else {
|
4369
|
+
i = node.childNodes.length - 1;
|
4370
|
+
while (i >= 0) {
|
4371
|
+
nodeStack.push(node.childNodes[i]);
|
4372
|
+
i -= 1;
|
4373
|
+
}
|
4374
|
+
}
|
4375
|
+
if (!stop) {
|
4376
|
+
node = nodeStack.pop();
|
4377
|
+
}
|
4378
|
+
}
|
4379
|
+
|
4380
|
+
sel = this.options.contentWindow.getSelection();
|
4381
|
+
sel.removeAllRanges();
|
4382
|
+
sel.addRange(range);
|
4383
|
+
},
|
4384
|
+
|
4385
|
+
createLink: function (opts) {
|
4386
|
+
var customEvent,
|
4387
|
+
i;
|
4388
|
+
|
4389
|
+
if (opts.url && opts.url.trim().length > 0) {
|
4390
|
+
this.options.ownerDocument.execCommand('createLink', false, opts.url);
|
4391
|
+
|
4392
|
+
if (this.options.targetBlank || opts.target === '_blank') {
|
4393
|
+
Util.setTargetBlank(Util.getSelectionStart(this.options.ownerDocument));
|
4394
|
+
}
|
4395
|
+
|
4396
|
+
if (opts.buttonClass) {
|
4397
|
+
Util.addClassToAnchors(Util.getSelectionStart(this.options.ownerDocument), opts.buttonClass);
|
4398
|
+
}
|
4399
|
+
}
|
4400
|
+
|
4401
|
+
if (this.options.targetBlank || opts.target === "_blank" || opts.buttonClass) {
|
4402
|
+
customEvent = this.options.ownerDocument.createEvent("HTMLEvents");
|
4403
|
+
customEvent.initEvent("input", true, true, this.options.contentWindow);
|
4404
|
+
for (i = 0; i < this.elements.length; i += 1) {
|
4405
|
+
this.elements[i].dispatchEvent(customEvent);
|
4406
|
+
}
|
4407
|
+
}
|
4408
|
+
},
|
4409
|
+
|
4410
|
+
// alias for setup - keeping for backwards compatability
|
4411
|
+
activate: function () {
|
4412
|
+
Util.deprecatedMethod.call(this, 'activate', 'setup', arguments, 'v5.0.0');
|
4413
|
+
},
|
4414
|
+
|
4415
|
+
// alias for destroy - keeping for backwards compatability
|
4416
|
+
deactivate: function () {
|
4417
|
+
Util.deprecatedMethod.call(this, 'deactivate', 'destroy', arguments, 'v5.0.0');
|
4418
|
+
},
|
4419
|
+
|
4420
|
+
cleanPaste: function (text) {
|
4421
|
+
this.getExtensionByName('paste').cleanPaste(text);
|
4422
|
+
},
|
4423
|
+
|
4424
|
+
pasteHTML: function (html, options) {
|
4425
|
+
this.getExtensionByName('paste').pasteHTML(html, options);
|
4426
|
+
}
|
4427
|
+
};
|
4428
|
+
}());
|
4429
|
+
|
4430
|
+
MediumEditor.version = (function(major, minor, revision) {
|
4431
|
+
return {
|
4432
|
+
major: parseInt(major, 10),
|
4433
|
+
minor: parseInt(minor, 10),
|
4434
|
+
revision: parseInt(revision, 10),
|
4435
|
+
toString: function(){
|
4436
|
+
return [major, minor, revision].join(".");
|
4437
|
+
}
|
4438
|
+
};
|
4439
|
+
}).apply(this, ({
|
4440
|
+
// grunt-bump looks for this:
|
4441
|
+
"version": "4.6.0"
|
4442
|
+
}).version.split("."));
|
4443
|
+
|
4444
|
+
return MediumEditor;
|
4445
|
+
}()));
|