medium-editor-rails 2.2.0 → 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/medium-editor-rails/version.rb +2 -2
- data/vendor/assets/javascripts/medium-editor.js +891 -247
- data/vendor/assets/stylesheets/medium-editor/medium-editor.css +20 -9
- data/vendor/assets/stylesheets/medium-editor/themes/flat.css +1 -1
- data/vendor/assets/stylesheets/medium-editor/themes/flat.min.css +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0241fca73542f5f5867edc588778dbcb10ad3f28
|
4
|
+
data.tar.gz: 842d1ca2ac98a5c6a45336d1f35816d6017d090a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e7a183c756bba1a1ac215348813f707b15e748cf94d8476fd214540dec9fdeefd89e98b8619f866845f1b22dd2368223d0bd1620cf32d758e843d06090f287cd
|
7
|
+
data.tar.gz: 4f03baf7dc8dbc0e08f5aa6e65273df20d0122160101901e7c34694913c482517b5599fae11ab9cbdb5f55e76646bdc09df89e3a28ba0ef02e670564a01e9463
|
data/README.md
CHANGED
@@ -8,7 +8,7 @@ This gem integrates [Medium Editor](https://github.com/yabwe/medium-editor) with
|
|
8
8
|
|
9
9
|
## Version
|
10
10
|
|
11
|
-
The latest version of Medium Editor bundled by this gem is [5.
|
11
|
+
The latest version of Medium Editor bundled by this gem is [5.15.0](https://github.com/yabwe/medium-editor/releases)
|
12
12
|
|
13
13
|
## Installation
|
14
14
|
|
@@ -385,7 +385,8 @@ if (!("classList" in document.createElement("_"))) {
|
|
385
385
|
|
386
386
|
(function (root, factory) {
|
387
387
|
'use strict';
|
388
|
-
|
388
|
+
var isElectron = typeof module === 'object' && process && process.versions && process.versions.electron;
|
389
|
+
if (!isElectron && typeof module === 'object') {
|
389
390
|
module.exports = factory;
|
390
391
|
} else if (typeof define === 'function' && define.amd) {
|
391
392
|
define(function () {
|
@@ -454,6 +455,7 @@ MediumEditor.extensions = {};
|
|
454
455
|
isMac: (window.navigator.platform.toUpperCase().indexOf('MAC') >= 0),
|
455
456
|
|
456
457
|
// https://github.com/jashkenas/underscore
|
458
|
+
// Lonely letter MUST USE the uppercase code
|
457
459
|
keyCode: {
|
458
460
|
BACKSPACE: 8,
|
459
461
|
TAB: 9,
|
@@ -462,7 +464,8 @@ MediumEditor.extensions = {};
|
|
462
464
|
SPACE: 32,
|
463
465
|
DELETE: 46,
|
464
466
|
K: 75, // K keycode, and not k
|
465
|
-
M: 77
|
467
|
+
M: 77,
|
468
|
+
V: 86
|
466
469
|
},
|
467
470
|
|
468
471
|
/**
|
@@ -897,8 +900,7 @@ MediumEditor.extensions = {};
|
|
897
900
|
range = range.cloneRange();
|
898
901
|
range.setStartAfter(lastNode);
|
899
902
|
range.collapse(true);
|
900
|
-
selection.
|
901
|
-
selection.addRange(range);
|
903
|
+
MediumEditor.selection.selectRange(doc, range);
|
902
904
|
}
|
903
905
|
res = true;
|
904
906
|
}
|
@@ -1428,11 +1430,15 @@ MediumEditor.extensions = {};
|
|
1428
1430
|
},
|
1429
1431
|
|
1430
1432
|
cleanupTags: function (el, tags) {
|
1431
|
-
tags.
|
1432
|
-
|
1433
|
-
|
1434
|
-
|
1435
|
-
|
1433
|
+
if (tags.indexOf(el.nodeName.toLowerCase()) !== -1) {
|
1434
|
+
el.parentNode.removeChild(el);
|
1435
|
+
}
|
1436
|
+
},
|
1437
|
+
|
1438
|
+
unwrapTags: function (el, tags) {
|
1439
|
+
if (tags.indexOf(el.nodeName.toLowerCase()) !== -1) {
|
1440
|
+
MediumEditor.util.unwrap(el, document);
|
1441
|
+
}
|
1436
1442
|
},
|
1437
1443
|
|
1438
1444
|
// get the closest parent
|
@@ -1457,6 +1463,17 @@ MediumEditor.extensions = {};
|
|
1457
1463
|
} else {
|
1458
1464
|
el.parentNode.removeChild(el);
|
1459
1465
|
}
|
1466
|
+
},
|
1467
|
+
|
1468
|
+
guid: function () {
|
1469
|
+
function _s4() {
|
1470
|
+
return Math
|
1471
|
+
.floor((1 + Math.random()) * 0x10000)
|
1472
|
+
.toString(16)
|
1473
|
+
.substring(1);
|
1474
|
+
}
|
1475
|
+
|
1476
|
+
return _s4() + _s4() + '-' + _s4() + '-' + _s4() + '-' + _s4() + '-' + _s4() + _s4() + _s4();
|
1460
1477
|
}
|
1461
1478
|
};
|
1462
1479
|
|
@@ -1641,6 +1658,19 @@ MediumEditor.extensions = {};
|
|
1641
1658
|
*/
|
1642
1659
|
setInactive: undefined,
|
1643
1660
|
|
1661
|
+
/* getInteractionElements: [function ()]
|
1662
|
+
*
|
1663
|
+
* If the extension renders any elements that the user can interact with,
|
1664
|
+
* this method should be implemented and return the root element or an array
|
1665
|
+
* containing all of the root elements. MediumEditor will call this function
|
1666
|
+
* during interaction to see if the user clicked on something outside of the editor.
|
1667
|
+
* The elements are used to check if the target element of a click or
|
1668
|
+
* other user event is a descendant of any extension elements.
|
1669
|
+
* This way, the editor can also count user interaction within editor elements as
|
1670
|
+
* interactions with the editor, and thus not trigger 'blur'
|
1671
|
+
*/
|
1672
|
+
getInteractionElements: undefined,
|
1673
|
+
|
1644
1674
|
/************************ Helpers ************************
|
1645
1675
|
* The following are helpers that are either set by MediumEditor
|
1646
1676
|
* during initialization, or are helper methods which either
|
@@ -1938,9 +1968,7 @@ MediumEditor.extensions = {};
|
|
1938
1968
|
range = this.importSelectionMoveCursorPastAnchor(selectionState, range);
|
1939
1969
|
}
|
1940
1970
|
|
1941
|
-
|
1942
|
-
sel.removeAllRanges();
|
1943
|
-
sel.addRange(range);
|
1971
|
+
this.selectRange(doc, range);
|
1944
1972
|
},
|
1945
1973
|
|
1946
1974
|
// Utility method called from importSelection only
|
@@ -2333,16 +2361,12 @@ MediumEditor.extensions = {};
|
|
2333
2361
|
},
|
2334
2362
|
|
2335
2363
|
selectNode: function (node, doc) {
|
2336
|
-
var range = doc.createRange()
|
2337
|
-
sel = doc.getSelection();
|
2338
|
-
|
2364
|
+
var range = doc.createRange();
|
2339
2365
|
range.selectNodeContents(node);
|
2340
|
-
|
2341
|
-
sel.addRange(range);
|
2366
|
+
this.selectRange(doc, range);
|
2342
2367
|
},
|
2343
2368
|
|
2344
2369
|
select: function (doc, startNode, startOffset, endNode, endOffset) {
|
2345
|
-
doc.getSelection().removeAllRanges();
|
2346
2370
|
var range = doc.createRange();
|
2347
2371
|
range.setStart(startNode, startOffset);
|
2348
2372
|
if (endNode) {
|
@@ -2350,7 +2374,7 @@ MediumEditor.extensions = {};
|
|
2350
2374
|
} else {
|
2351
2375
|
range.collapse(true);
|
2352
2376
|
}
|
2353
|
-
|
2377
|
+
this.selectRange(doc, range);
|
2354
2378
|
return range;
|
2355
2379
|
},
|
2356
2380
|
|
@@ -2387,6 +2411,13 @@ MediumEditor.extensions = {};
|
|
2387
2411
|
return selection.getRangeAt(0);
|
2388
2412
|
},
|
2389
2413
|
|
2414
|
+
selectRange: function (ownerDocument, range) {
|
2415
|
+
var selection = ownerDocument.getSelection();
|
2416
|
+
|
2417
|
+
selection.removeAllRanges();
|
2418
|
+
selection.addRange(range);
|
2419
|
+
},
|
2420
|
+
|
2390
2421
|
// http://stackoverflow.com/questions/1197401/how-can-i-get-the-element-the-caret-is-in-with-javascript-when-using-contentedi
|
2391
2422
|
// by You
|
2392
2423
|
getSelectionStart: function (ownerDocument) {
|
@@ -2403,6 +2434,26 @@ MediumEditor.extensions = {};
|
|
2403
2434
|
(function () {
|
2404
2435
|
'use strict';
|
2405
2436
|
|
2437
|
+
function isElementDescendantOfExtension(extensions, element) {
|
2438
|
+
return extensions.some(function (extension) {
|
2439
|
+
if (typeof extension.getInteractionElements !== 'function') {
|
2440
|
+
return false;
|
2441
|
+
}
|
2442
|
+
|
2443
|
+
var extensionElements = extension.getInteractionElements();
|
2444
|
+
if (!extensionElements) {
|
2445
|
+
return false;
|
2446
|
+
}
|
2447
|
+
|
2448
|
+
if (!Array.isArray(extensionElements)) {
|
2449
|
+
extensionElements = [extensionElements];
|
2450
|
+
}
|
2451
|
+
return extensionElements.some(function (el) {
|
2452
|
+
return MediumEditor.util.isDescendant(el, element, true);
|
2453
|
+
});
|
2454
|
+
});
|
2455
|
+
}
|
2456
|
+
|
2406
2457
|
var Events = function (instance) {
|
2407
2458
|
this.base = instance;
|
2408
2459
|
this.options = this.base.options;
|
@@ -2417,18 +2468,32 @@ MediumEditor.extensions = {};
|
|
2417
2468
|
|
2418
2469
|
// Helpers for event handling
|
2419
2470
|
|
2420
|
-
attachDOMEvent: function (
|
2421
|
-
|
2422
|
-
|
2471
|
+
attachDOMEvent: function (targets, event, listener, useCapture) {
|
2472
|
+
var win = this.base.options.contentWindow,
|
2473
|
+
doc = this.base.options.ownerDocument;
|
2474
|
+
|
2475
|
+
targets = MediumEditor.util.isElement(targets) || [win, doc].indexOf(targets) > -1 ? [targets] : targets;
|
2476
|
+
|
2477
|
+
Array.prototype.forEach.call(targets, function (target) {
|
2478
|
+
target.addEventListener(event, listener, useCapture);
|
2479
|
+
this.events.push([target, event, listener, useCapture]);
|
2480
|
+
}.bind(this));
|
2423
2481
|
},
|
2424
2482
|
|
2425
|
-
detachDOMEvent: function (
|
2426
|
-
var index
|
2427
|
-
|
2428
|
-
|
2429
|
-
|
2430
|
-
|
2431
|
-
|
2483
|
+
detachDOMEvent: function (targets, event, listener, useCapture) {
|
2484
|
+
var index, e,
|
2485
|
+
win = this.base.options.contentWindow,
|
2486
|
+
doc = this.base.options.ownerDocument;
|
2487
|
+
|
2488
|
+
targets = MediumEditor.util.isElement(targets) || [win, doc].indexOf(targets) > -1 ? [targets] : targets;
|
2489
|
+
|
2490
|
+
Array.prototype.forEach.call(targets, function (target) {
|
2491
|
+
index = this.indexOfListener(target, event, listener, useCapture);
|
2492
|
+
if (index !== -1) {
|
2493
|
+
e = this.events.splice(index, 1)[0];
|
2494
|
+
e[0].removeEventListener(e[1], e[2], e[3]);
|
2495
|
+
}
|
2496
|
+
}.bind(this));
|
2432
2497
|
},
|
2433
2498
|
|
2434
2499
|
indexOfListener: function (target, event, listener, useCapture) {
|
@@ -2450,6 +2515,30 @@ MediumEditor.extensions = {};
|
|
2450
2515
|
}
|
2451
2516
|
},
|
2452
2517
|
|
2518
|
+
detachAllEventsFromElement: function (element) {
|
2519
|
+
var filtered = this.events.filter(function (e) {
|
2520
|
+
return e && e[0].getAttribute && e[0].getAttribute('medium-editor-index') === element.getAttribute('medium-editor-index');
|
2521
|
+
});
|
2522
|
+
|
2523
|
+
for (var i = 0, len = filtered.length; i < len; i++) {
|
2524
|
+
var e = filtered[i];
|
2525
|
+
this.detachDOMEvent(e[0], e[1], e[2], e[3]);
|
2526
|
+
}
|
2527
|
+
},
|
2528
|
+
|
2529
|
+
// Attach all existing handlers to a new element
|
2530
|
+
attachAllEventsToElement: function (element) {
|
2531
|
+
if (this.listeners['editableInput']) {
|
2532
|
+
this.contentCache[element.getAttribute('medium-editor-index')] = element.innerHTML;
|
2533
|
+
}
|
2534
|
+
|
2535
|
+
if (this.eventsCache) {
|
2536
|
+
this.eventsCache.forEach(function (e) {
|
2537
|
+
this.attachDOMEvent(element, e['name'], e['handler'].bind(this));
|
2538
|
+
}, this);
|
2539
|
+
}
|
2540
|
+
},
|
2541
|
+
|
2453
2542
|
enableCustomEvent: function (event) {
|
2454
2543
|
if (this.disabledEvents[event] !== undefined) {
|
2455
2544
|
delete this.disabledEvents[event];
|
@@ -2564,23 +2653,23 @@ MediumEditor.extensions = {};
|
|
2564
2653
|
|
2565
2654
|
// Helper method to call all listeners to execCommand
|
2566
2655
|
var callListeners = function (args, result) {
|
2567
|
-
|
2568
|
-
|
2569
|
-
|
2570
|
-
|
2571
|
-
|
2572
|
-
|
2573
|
-
|
2574
|
-
});
|
2656
|
+
if (doc.execCommand.listeners) {
|
2657
|
+
doc.execCommand.listeners.forEach(function (listener) {
|
2658
|
+
listener({
|
2659
|
+
command: args[0],
|
2660
|
+
value: args[2],
|
2661
|
+
args: args,
|
2662
|
+
result: result
|
2575
2663
|
});
|
2576
|
-
}
|
2577
|
-
}
|
2664
|
+
});
|
2665
|
+
}
|
2666
|
+
},
|
2578
2667
|
|
2579
|
-
|
2580
|
-
|
2581
|
-
|
2582
|
-
|
2583
|
-
|
2668
|
+
// Create a wrapper method for execCommand which will:
|
2669
|
+
// 1) Call document.execCommand with the correct arguments
|
2670
|
+
// 2) Loop through any listeners and notify them that execCommand was called
|
2671
|
+
// passing extra info on the call
|
2672
|
+
// 3) Return the result
|
2584
2673
|
wrapper = function () {
|
2585
2674
|
var result = doc.execCommand.orig.apply(this, arguments);
|
2586
2675
|
|
@@ -2641,15 +2730,15 @@ MediumEditor.extensions = {};
|
|
2641
2730
|
break;
|
2642
2731
|
case 'editableInput':
|
2643
2732
|
// setup cache for knowing when the content has changed
|
2644
|
-
this.contentCache =
|
2733
|
+
this.contentCache = {};
|
2645
2734
|
this.base.elements.forEach(function (element) {
|
2646
2735
|
this.contentCache[element.getAttribute('medium-editor-index')] = element.innerHTML;
|
2736
|
+
}, this);
|
2647
2737
|
|
2648
|
-
|
2649
|
-
|
2650
|
-
|
2651
|
-
|
2652
|
-
}.bind(this));
|
2738
|
+
// Attach to the 'oninput' event, handled correctly by most browsers
|
2739
|
+
if (this.InputEventOnContenteditableSupported) {
|
2740
|
+
this.attachToEachElement('input', this.handleInput);
|
2741
|
+
}
|
2653
2742
|
|
2654
2743
|
// For browsers which don't support the input event on contenteditable (IE)
|
2655
2744
|
// we'll attach to 'selectionchange' on the document and 'keypress' on the editables
|
@@ -2710,6 +2799,8 @@ MediumEditor.extensions = {};
|
|
2710
2799
|
// Detecting drop on the contenteditables
|
2711
2800
|
this.attachToEachElement('drop', this.handleDrop);
|
2712
2801
|
break;
|
2802
|
+
// TODO: We need to have a custom 'paste' event separate from 'editablePaste'
|
2803
|
+
// Need to think about the way to introduce this without breaking folks
|
2713
2804
|
case 'editablePaste':
|
2714
2805
|
// Detecting paste on the contenteditables
|
2715
2806
|
this.attachToEachElement('paste', this.handlePaste);
|
@@ -2719,9 +2810,26 @@ MediumEditor.extensions = {};
|
|
2719
2810
|
},
|
2720
2811
|
|
2721
2812
|
attachToEachElement: function (name, handler) {
|
2813
|
+
// build our internal cache to know which element got already what handler attached
|
2814
|
+
if (!this.eventsCache) {
|
2815
|
+
this.eventsCache = [];
|
2816
|
+
}
|
2817
|
+
|
2722
2818
|
this.base.elements.forEach(function (element) {
|
2723
2819
|
this.attachDOMEvent(element, name, handler.bind(this));
|
2724
2820
|
}, this);
|
2821
|
+
|
2822
|
+
this.eventsCache.push({ 'name': name, 'handler': handler });
|
2823
|
+
},
|
2824
|
+
|
2825
|
+
cleanupElement: function (element) {
|
2826
|
+
var index = element.getAttribute('medium-editor-index');
|
2827
|
+
if (index) {
|
2828
|
+
this.detachAllEventsFromElement(element);
|
2829
|
+
if (this.contentCache) {
|
2830
|
+
delete this.contentCache[index];
|
2831
|
+
}
|
2832
|
+
}
|
2725
2833
|
},
|
2726
2834
|
|
2727
2835
|
focusElement: function (element) {
|
@@ -2730,21 +2838,16 @@ MediumEditor.extensions = {};
|
|
2730
2838
|
},
|
2731
2839
|
|
2732
2840
|
updateFocus: function (target, eventObj) {
|
2733
|
-
var
|
2734
|
-
toolbarEl = toolbar ? toolbar.getToolbarElement() : null,
|
2735
|
-
anchorPreview = this.base.getExtensionByName('anchor-preview'),
|
2736
|
-
previewEl = (anchorPreview && anchorPreview.getPreviewElement) ? anchorPreview.getPreviewElement() : null,
|
2737
|
-
hadFocus = this.base.getFocusedElement(),
|
2841
|
+
var hadFocus = this.base.getFocusedElement(),
|
2738
2842
|
toFocus;
|
2739
2843
|
|
2740
|
-
// For clicks, we need to know if the mousedown that caused the click happened inside the existing focused element
|
2741
|
-
// If so, we don't want to focus another element
|
2844
|
+
// For clicks, we need to know if the mousedown that caused the click happened inside the existing focused element
|
2845
|
+
// or one of the extension elements. If so, we don't want to focus another element
|
2742
2846
|
if (hadFocus &&
|
2743
|
-
|
2744
|
-
|
2745
|
-
|
2746
|
-
|
2747
|
-
MediumEditor.util.isDescendant(previewEl, this.lastMousedownTarget, true))) {
|
2847
|
+
eventObj.type === 'click' &&
|
2848
|
+
this.lastMousedownTarget &&
|
2849
|
+
(MediumEditor.util.isDescendant(hadFocus, this.lastMousedownTarget, true) ||
|
2850
|
+
isElementDescendantOfExtension(this.base.extensions, this.lastMousedownTarget))) {
|
2748
2851
|
toFocus = hadFocus;
|
2749
2852
|
}
|
2750
2853
|
|
@@ -2760,10 +2863,9 @@ MediumEditor.extensions = {};
|
|
2760
2863
|
}, this);
|
2761
2864
|
}
|
2762
2865
|
|
2763
|
-
// Check if the target is external (not part of the editor, toolbar, or
|
2866
|
+
// Check if the target is external (not part of the editor, toolbar, or any other extension)
|
2764
2867
|
var externalEvent = !MediumEditor.util.isDescendant(hadFocus, target, true) &&
|
2765
|
-
|
2766
|
-
!MediumEditor.util.isDescendant(previewEl, target, true);
|
2868
|
+
!isElementDescendantOfExtension(this.base.extensions, target);
|
2767
2869
|
|
2768
2870
|
if (toFocus !== hadFocus) {
|
2769
2871
|
// If element has focus, and focus is going outside of editor
|
@@ -2793,12 +2895,14 @@ MediumEditor.extensions = {};
|
|
2793
2895
|
}
|
2794
2896
|
// An event triggered which signifies that the user may have changed someting
|
2795
2897
|
// Look in our cache of input for the contenteditables to see if something changed
|
2796
|
-
var index = target.getAttribute('medium-editor-index')
|
2797
|
-
|
2898
|
+
var index = target.getAttribute('medium-editor-index'),
|
2899
|
+
html = target.innerHTML;
|
2900
|
+
|
2901
|
+
if (html !== this.contentCache[index]) {
|
2798
2902
|
// The content has changed since the last time we checked, fire the event
|
2799
2903
|
this.triggerCustomEvent('editableInput', eventObj, target);
|
2800
2904
|
}
|
2801
|
-
this.contentCache[index] =
|
2905
|
+
this.contentCache[index] = html;
|
2802
2906
|
},
|
2803
2907
|
|
2804
2908
|
handleDocumentSelectionChange: function (event) {
|
@@ -3679,12 +3783,12 @@ MediumEditor.extensions = {};
|
|
3679
3783
|
targetCheckbox = this.getAnchorTargetCheckbox(),
|
3680
3784
|
buttonCheckbox = this.getAnchorButtonCheckbox();
|
3681
3785
|
|
3682
|
-
opts = opts || {
|
3786
|
+
opts = opts || { value: '' };
|
3683
3787
|
// TODO: This is for backwards compatability
|
3684
3788
|
// We don't need to support the 'string' argument in 6.0.0
|
3685
3789
|
if (typeof opts === 'string') {
|
3686
3790
|
opts = {
|
3687
|
-
|
3791
|
+
value: opts
|
3688
3792
|
};
|
3689
3793
|
}
|
3690
3794
|
|
@@ -3693,7 +3797,7 @@ MediumEditor.extensions = {};
|
|
3693
3797
|
MediumEditor.extensions.form.prototype.showForm.apply(this);
|
3694
3798
|
this.setToolbarPosition();
|
3695
3799
|
|
3696
|
-
input.value = opts.
|
3800
|
+
input.value = opts.value;
|
3697
3801
|
input.focus();
|
3698
3802
|
|
3699
3803
|
// If we have a target checkbox, we want it to be checked/unchecked
|
@@ -3730,11 +3834,11 @@ MediumEditor.extensions = {};
|
|
3730
3834
|
var targetCheckbox = this.getAnchorTargetCheckbox(),
|
3731
3835
|
buttonCheckbox = this.getAnchorButtonCheckbox(),
|
3732
3836
|
opts = {
|
3733
|
-
|
3837
|
+
value: this.getInput().value.trim()
|
3734
3838
|
};
|
3735
3839
|
|
3736
3840
|
if (this.linkValidation) {
|
3737
|
-
opts.
|
3841
|
+
opts.value = this.checkLinkFormat(opts.value);
|
3738
3842
|
}
|
3739
3843
|
|
3740
3844
|
opts.target = '_self';
|
@@ -3764,14 +3868,15 @@ MediumEditor.extensions = {};
|
|
3764
3868
|
// Matches any alphabetical characters followed by ://
|
3765
3869
|
// Matches protocol relative "//"
|
3766
3870
|
// Matches common external protocols "mailto:" "tel:" "maps:"
|
3767
|
-
|
3871
|
+
// Matches relative hash link, begins with "#"
|
3872
|
+
var urlSchemeRegex = /^([a-z]+:)?\/\/|^(mailto|tel|maps):|^\#/i,
|
3768
3873
|
// var te is a regex for checking if the string is a telephone number
|
3769
3874
|
telRegex = /^\+?\s?\(?(?:\d\s?\-?\)?){3,20}$/;
|
3770
3875
|
if (telRegex.test(value)) {
|
3771
3876
|
return 'tel:' + value;
|
3772
3877
|
} else {
|
3773
3878
|
// Check for URL scheme and default to http:// if none found
|
3774
|
-
return (urlSchemeRegex.test(value) ? '' : 'http://') + value;
|
3879
|
+
return (urlSchemeRegex.test(value) ? '' : 'http://') + encodeURI(value);
|
3775
3880
|
}
|
3776
3881
|
},
|
3777
3882
|
|
@@ -3884,6 +3989,11 @@ MediumEditor.extensions = {};
|
|
3884
3989
|
*/
|
3885
3990
|
showWhenToolbarIsVisible: false,
|
3886
3991
|
|
3992
|
+
/* showOnEmptyLinks: [boolean]
|
3993
|
+
* determines whether the anchor tag preview shows up on links with href="" or href="#something"
|
3994
|
+
*/
|
3995
|
+
showOnEmptyLinks: true,
|
3996
|
+
|
3887
3997
|
init: function () {
|
3888
3998
|
this.anchorPreview = this.createPreview();
|
3889
3999
|
|
@@ -3892,6 +4002,11 @@ MediumEditor.extensions = {};
|
|
3892
4002
|
this.attachToEditables();
|
3893
4003
|
},
|
3894
4004
|
|
4005
|
+
getInteractionElements: function () {
|
4006
|
+
return this.getPreviewElement();
|
4007
|
+
},
|
4008
|
+
|
4009
|
+
// TODO: Remove this function in 6.0.0
|
3895
4010
|
getPreviewElement: function () {
|
3896
4011
|
return this.anchorPreview;
|
3897
4012
|
},
|
@@ -3956,13 +4071,15 @@ MediumEditor.extensions = {};
|
|
3956
4071
|
|
3957
4072
|
positionPreview: function (activeAnchor) {
|
3958
4073
|
activeAnchor = activeAnchor || this.activeAnchor;
|
3959
|
-
var
|
4074
|
+
var containerWidth = this.window.innerWidth,
|
4075
|
+
buttonHeight = this.anchorPreview.offsetHeight,
|
3960
4076
|
boundary = activeAnchor.getBoundingClientRect(),
|
3961
|
-
middleBoundary = (boundary.left + boundary.right) / 2,
|
3962
4077
|
diffLeft = this.diffLeft,
|
3963
4078
|
diffTop = this.diffTop,
|
3964
|
-
|
3965
|
-
|
4079
|
+
elementsContainer = this.getEditorOption('elementsContainer'),
|
4080
|
+
elementsContainerAbsolute = ['absolute', 'fixed'].indexOf(window.getComputedStyle(elementsContainer).getPropertyValue('position')) > -1,
|
4081
|
+
relativeBoundary = {},
|
4082
|
+
halfOffsetWidth, defaultLeft, middleBoundary, elementsContainerBoundary, top;
|
3966
4083
|
|
3967
4084
|
halfOffsetWidth = this.anchorPreview.offsetWidth / 2;
|
3968
4085
|
var toolbarExtension = this.base.getExtensionByName('toolbar');
|
@@ -3972,12 +4089,35 @@ MediumEditor.extensions = {};
|
|
3972
4089
|
}
|
3973
4090
|
defaultLeft = diffLeft - halfOffsetWidth;
|
3974
4091
|
|
3975
|
-
|
4092
|
+
// If container element is absolute / fixed, recalculate boundaries to be relative to the container
|
4093
|
+
if (elementsContainerAbsolute) {
|
4094
|
+
elementsContainerBoundary = elementsContainer.getBoundingClientRect();
|
4095
|
+
['top', 'left'].forEach(function (key) {
|
4096
|
+
relativeBoundary[key] = boundary[key] - elementsContainerBoundary[key];
|
4097
|
+
});
|
4098
|
+
|
4099
|
+
relativeBoundary.width = boundary.width;
|
4100
|
+
relativeBoundary.height = boundary.height;
|
4101
|
+
boundary = relativeBoundary;
|
4102
|
+
|
4103
|
+
containerWidth = elementsContainerBoundary.width;
|
4104
|
+
|
4105
|
+
// Adjust top position according to container scroll position
|
4106
|
+
top = elementsContainer.scrollTop;
|
4107
|
+
} else {
|
4108
|
+
// Adjust top position according to window scroll position
|
4109
|
+
top = this.window.pageYOffset;
|
4110
|
+
}
|
4111
|
+
|
4112
|
+
middleBoundary = boundary.left + boundary.width / 2;
|
4113
|
+
top += buttonHeight + boundary.top + boundary.height - diffTop - this.anchorPreview.offsetHeight;
|
4114
|
+
|
4115
|
+
this.anchorPreview.style.top = Math.round(top) + 'px';
|
3976
4116
|
this.anchorPreview.style.right = 'initial';
|
3977
4117
|
if (middleBoundary < halfOffsetWidth) {
|
3978
4118
|
this.anchorPreview.style.left = defaultLeft + halfOffsetWidth + 'px';
|
3979
4119
|
this.anchorPreview.style.right = 'initial';
|
3980
|
-
} else if ((
|
4120
|
+
} else if ((containerWidth - middleBoundary) < halfOffsetWidth) {
|
3981
4121
|
this.anchorPreview.style.left = 'auto';
|
3982
4122
|
this.anchorPreview.style.right = 0;
|
3983
4123
|
} else {
|
@@ -3988,6 +4128,15 @@ MediumEditor.extensions = {};
|
|
3988
4128
|
|
3989
4129
|
attachToEditables: function () {
|
3990
4130
|
this.subscribe('editableMouseover', this.handleEditableMouseover.bind(this));
|
4131
|
+
this.subscribe('positionedToolbar', this.handlePositionedToolbar.bind(this));
|
4132
|
+
},
|
4133
|
+
|
4134
|
+
handlePositionedToolbar: function () {
|
4135
|
+
// If the toolbar is visible and positioned, we don't need to hide the preview
|
4136
|
+
// when showWhenToolbarIsVisible is true
|
4137
|
+
if (!this.showWhenToolbarIsVisible) {
|
4138
|
+
this.hidePreview();
|
4139
|
+
}
|
3991
4140
|
},
|
3992
4141
|
|
3993
4142
|
handleClick: function (event) {
|
@@ -4004,7 +4153,7 @@ MediumEditor.extensions = {};
|
|
4004
4153
|
this.base.delay(function () {
|
4005
4154
|
if (activeAnchor) {
|
4006
4155
|
var opts = {
|
4007
|
-
|
4156
|
+
value: activeAnchor.attributes.href.value,
|
4008
4157
|
target: activeAnchor.getAttribute('target'),
|
4009
4158
|
buttonClass: activeAnchor.getAttribute('class')
|
4010
4159
|
};
|
@@ -4033,7 +4182,8 @@ MediumEditor.extensions = {};
|
|
4033
4182
|
// Detect empty href attributes
|
4034
4183
|
// The browser will make href="" or href="#top"
|
4035
4184
|
// into absolute urls when accessed as event.target.href, so check the html
|
4036
|
-
if (
|
4185
|
+
if (!this.showOnEmptyLinks &&
|
4186
|
+
(!/href=["']\S+["']/.test(target.outerHTML) || /href=["']#\S+["']/.test(target.outerHTML))) {
|
4037
4187
|
return true;
|
4038
4188
|
}
|
4039
4189
|
|
@@ -4537,8 +4687,12 @@ MediumEditor.extensions = {};
|
|
4537
4687
|
event.preventDefault();
|
4538
4688
|
event.stopPropagation();
|
4539
4689
|
|
4690
|
+
// command can be a function to execute
|
4691
|
+
if (typeof data.command === 'function') {
|
4692
|
+
data.command.apply(this);
|
4693
|
+
}
|
4540
4694
|
// command can be false so the shortcut is just disabled
|
4541
|
-
if (false !== data.command) {
|
4695
|
+
else if (false !== data.command) {
|
4542
4696
|
this.execAction(data.command);
|
4543
4697
|
}
|
4544
4698
|
}
|
@@ -4709,7 +4863,7 @@ MediumEditor.extensions = {};
|
|
4709
4863
|
if (font === '') {
|
4710
4864
|
this.clearFontName();
|
4711
4865
|
} else {
|
4712
|
-
this.execAction('fontName', {
|
4866
|
+
this.execAction('fontName', { value: font });
|
4713
4867
|
}
|
4714
4868
|
},
|
4715
4869
|
|
@@ -4887,7 +5041,7 @@ MediumEditor.extensions = {};
|
|
4887
5041
|
if (size === '4') {
|
4888
5042
|
this.clearFontSize();
|
4889
5043
|
} else {
|
4890
|
-
this.execAction('fontSize', {
|
5044
|
+
this.execAction('fontSize', { value: size });
|
4891
5045
|
}
|
4892
5046
|
},
|
4893
5047
|
|
@@ -4913,6 +5067,16 @@ MediumEditor.extensions = {};
|
|
4913
5067
|
}());
|
4914
5068
|
(function () {
|
4915
5069
|
'use strict';
|
5070
|
+
|
5071
|
+
/* Helpers and internal variables that don't need to be members of actual paste handler */
|
5072
|
+
|
5073
|
+
var pasteBinDefaultContent = '%ME_PASTEBIN%',
|
5074
|
+
lastRange = null,
|
5075
|
+
keyboardPasteEditable = null,
|
5076
|
+
stopProp = function (event) {
|
5077
|
+
event.stopPropagation();
|
5078
|
+
};
|
5079
|
+
|
4916
5080
|
/*jslint regexp: true*/
|
4917
5081
|
/*
|
4918
5082
|
jslint does not allow character negation, because the negation
|
@@ -4922,6 +5086,15 @@ MediumEditor.extensions = {};
|
|
4922
5086
|
*/
|
4923
5087
|
function createReplacements() {
|
4924
5088
|
return [
|
5089
|
+
// Remove anything but the contents within the BODY element
|
5090
|
+
[new RegExp(/^[\s\S]*<body[^>]*>\s*|\s*<\/body[^>]*>[\s\S]*$/g), ''],
|
5091
|
+
|
5092
|
+
// cleanup comments added by Chrome when pasting html
|
5093
|
+
[new RegExp(/<!--StartFragment-->|<!--EndFragment-->/g), ''],
|
5094
|
+
|
5095
|
+
// Trailing BR elements
|
5096
|
+
[new RegExp(/<br>$/i), ''],
|
5097
|
+
|
4925
5098
|
// replace two bogus tags that begin pastes from google docs
|
4926
5099
|
[new RegExp(/<[^>]*docs-internal-guid[^>]*>/gi), ''],
|
4927
5100
|
[new RegExp(/<\/b>(<br[^>]*>)?$/gi), ''],
|
@@ -4931,13 +5104,13 @@ MediumEditor.extensions = {};
|
|
4931
5104
|
[new RegExp(/<br class="Apple-interchange-newline">/g), '<br>'],
|
4932
5105
|
|
4933
5106
|
// replace google docs italics+bold with a span to be replaced once the html is inserted
|
4934
|
-
[new RegExp(/<span[^>]*(font-style:italic;font-weight:bold|font-weight:bold;font-style:italic)[^>]*>/gi), '<span class="replace-with italic bold">'],
|
5107
|
+
[new RegExp(/<span[^>]*(font-style:italic;font-weight:(bold|700)|font-weight:(bold|700);font-style:italic)[^>]*>/gi), '<span class="replace-with italic bold">'],
|
4935
5108
|
|
4936
5109
|
// replace google docs italics with a span to be replaced once the html is inserted
|
4937
5110
|
[new RegExp(/<span[^>]*font-style:italic[^>]*>/gi), '<span class="replace-with italic">'],
|
4938
5111
|
|
4939
5112
|
//[replace google docs bolds with a span to be replaced once the html is inserted
|
4940
|
-
[new RegExp(/<span[^>]*font-weight:bold[^>]*>/gi), '<span class="replace-with bold">'],
|
5113
|
+
[new RegExp(/<span[^>]*font-weight:(bold|700)[^>]*>/gi), '<span class="replace-with bold">'],
|
4941
5114
|
|
4942
5115
|
// replace manually entered b/i/a tags with real ones
|
4943
5116
|
[new RegExp(/<(\/?)(i|b|a)>/gi), '<$1$2>'],
|
@@ -4953,13 +5126,47 @@ MediumEditor.extensions = {};
|
|
4953
5126
|
// Microsoft Word makes these odd tags, like <o:p></o:p>
|
4954
5127
|
[new RegExp(/<\/?o:[a-z]*>/gi), ''],
|
4955
5128
|
|
4956
|
-
//
|
4957
|
-
[
|
4958
|
-
['<!--StartFragment-->', '']
|
5129
|
+
// Microsoft Word adds some special elements around list items
|
5130
|
+
[new RegExp(/<!\[if !supportLists\]>(((?!<!).)*)<!\[endif]\>/gi), '$1']
|
4959
5131
|
];
|
4960
5132
|
}
|
4961
5133
|
/*jslint regexp: false*/
|
4962
5134
|
|
5135
|
+
/**
|
5136
|
+
* Gets various content types out of the Clipboard API. It will also get the
|
5137
|
+
* plain text using older IE and WebKit API.
|
5138
|
+
*
|
5139
|
+
* @param {event} event Event fired on paste.
|
5140
|
+
* @param {win} reference to window
|
5141
|
+
* @param {doc} reference to document
|
5142
|
+
* @return {Object} Object with mime types and data for those mime types.
|
5143
|
+
*/
|
5144
|
+
function getClipboardContent(event, win, doc) {
|
5145
|
+
var dataTransfer = event.clipboardData || win.clipboardData || doc.dataTransfer,
|
5146
|
+
data = {};
|
5147
|
+
|
5148
|
+
if (!dataTransfer) {
|
5149
|
+
return data;
|
5150
|
+
}
|
5151
|
+
|
5152
|
+
// Use old WebKit/IE API
|
5153
|
+
if (dataTransfer.getData) {
|
5154
|
+
var legacyText = dataTransfer.getData('Text');
|
5155
|
+
if (legacyText && legacyText.length > 0) {
|
5156
|
+
data['text/plain'] = legacyText;
|
5157
|
+
}
|
5158
|
+
}
|
5159
|
+
|
5160
|
+
if (dataTransfer.types) {
|
5161
|
+
for (var i = 0; i < dataTransfer.types.length; i++) {
|
5162
|
+
var contentType = dataTransfer.types[i];
|
5163
|
+
data[contentType] = dataTransfer.getData(contentType);
|
5164
|
+
}
|
5165
|
+
}
|
5166
|
+
|
5167
|
+
return data;
|
5168
|
+
}
|
5169
|
+
|
4963
5170
|
var PasteHandler = MediumEditor.Extension.extend({
|
4964
5171
|
/* Paste Options */
|
4965
5172
|
|
@@ -4999,65 +5206,247 @@ MediumEditor.extensions = {};
|
|
4999
5206
|
*/
|
5000
5207
|
cleanTags: ['meta'],
|
5001
5208
|
|
5209
|
+
/* unwrapTags: [Array]
|
5210
|
+
* list of element tag names to unwrap (remove the element tag but retain its child elements)
|
5211
|
+
* during paste when __cleanPastedHTML__ is `true` or when
|
5212
|
+
* calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods.
|
5213
|
+
*/
|
5214
|
+
unwrapTags: [],
|
5215
|
+
|
5002
5216
|
init: function () {
|
5003
5217
|
MediumEditor.Extension.prototype.init.apply(this, arguments);
|
5004
5218
|
|
5005
5219
|
if (this.forcePlainText || this.cleanPastedHTML) {
|
5006
|
-
this.subscribe('
|
5220
|
+
this.subscribe('editableKeydown', this.handleKeydown.bind(this));
|
5221
|
+
// We need access to the full event data in paste
|
5222
|
+
// so we can't use the editablePaste event here
|
5223
|
+
this.getEditorElements().forEach(function (element) {
|
5224
|
+
this.on(element, 'paste', this.handlePaste.bind(this));
|
5225
|
+
}, this);
|
5226
|
+
this.subscribe('addElement', this.handleAddElement.bind(this));
|
5007
5227
|
}
|
5008
5228
|
},
|
5009
5229
|
|
5010
|
-
|
5011
|
-
|
5012
|
-
|
5013
|
-
|
5014
|
-
|
5015
|
-
|
5016
|
-
|
5017
|
-
|
5018
|
-
|
5019
|
-
|
5020
|
-
|
5230
|
+
handleAddElement: function (event, editable) {
|
5231
|
+
this.on(editable, 'paste', this.handlePaste.bind(this));
|
5232
|
+
},
|
5233
|
+
|
5234
|
+
destroy: function () {
|
5235
|
+
// Make sure pastebin is destroyed in case it's still around for some reason
|
5236
|
+
if (this.forcePlainText || this.cleanPastedHTML) {
|
5237
|
+
this.removePasteBin();
|
5238
|
+
}
|
5239
|
+
},
|
5240
|
+
|
5241
|
+
handlePaste: function (event, editable) {
|
5242
|
+
if (event.defaultPrevented) {
|
5243
|
+
return;
|
5244
|
+
}
|
5245
|
+
|
5246
|
+
var clipboardContent = getClipboardContent(event, this.window, this.document),
|
5247
|
+
pastedHTML = clipboardContent['text/html'],
|
5248
|
+
pastedPlain = clipboardContent['text/plain'];
|
5249
|
+
|
5250
|
+
if (this.window.clipboardData && event.clipboardData === undefined && !pastedHTML) {
|
5021
5251
|
// If window.clipboardData exists, but event.clipboardData doesn't exist,
|
5022
5252
|
// we're probably in IE. IE only has two possibilities for clipboard
|
5023
5253
|
// data format: 'Text' and 'URL'.
|
5024
5254
|
//
|
5025
|
-
//
|
5026
|
-
|
5027
|
-
dataFormatPlain = 'Text';
|
5255
|
+
// For IE, we'll fallback to 'Text' for text/html
|
5256
|
+
pastedHTML = pastedPlain;
|
5028
5257
|
}
|
5029
5258
|
|
5030
|
-
if (
|
5031
|
-
event.clipboardData.getData &&
|
5032
|
-
!event.defaultPrevented) {
|
5259
|
+
if (pastedHTML || pastedPlain) {
|
5033
5260
|
event.preventDefault();
|
5034
5261
|
|
5035
|
-
pastedHTML
|
5036
|
-
|
5262
|
+
this.doPaste(pastedHTML, pastedPlain, editable);
|
5263
|
+
}
|
5264
|
+
},
|
5037
5265
|
|
5038
|
-
|
5039
|
-
|
5040
|
-
|
5266
|
+
doPaste: function (pastedHTML, pastedPlain, editable) {
|
5267
|
+
var paragraphs,
|
5268
|
+
html = '',
|
5269
|
+
p;
|
5041
5270
|
|
5042
|
-
|
5043
|
-
|
5044
|
-
|
5045
|
-
|
5046
|
-
|
5047
|
-
|
5048
|
-
|
5049
|
-
|
5271
|
+
if (this.cleanPastedHTML && pastedHTML) {
|
5272
|
+
return this.cleanPaste(pastedHTML);
|
5273
|
+
}
|
5274
|
+
|
5275
|
+
if (!(this.getEditorOption('disableReturn') || (editable && editable.getAttribute('data-disable-return')))) {
|
5276
|
+
paragraphs = pastedPlain.split(/[\r\n]+/g);
|
5277
|
+
// If there are no \r\n in data, don't wrap in <p>
|
5278
|
+
if (paragraphs.length > 1) {
|
5279
|
+
for (p = 0; p < paragraphs.length; p += 1) {
|
5280
|
+
if (paragraphs[p] !== '') {
|
5281
|
+
html += '<p>' + MediumEditor.util.htmlEntities(paragraphs[p]) + '</p>';
|
5050
5282
|
}
|
5051
|
-
} else {
|
5052
|
-
html = MediumEditor.util.htmlEntities(paragraphs[0]);
|
5053
5283
|
}
|
5054
5284
|
} else {
|
5055
|
-
html = MediumEditor.util.htmlEntities(
|
5285
|
+
html = MediumEditor.util.htmlEntities(paragraphs[0]);
|
5056
5286
|
}
|
5057
|
-
|
5287
|
+
} else {
|
5288
|
+
html = MediumEditor.util.htmlEntities(pastedPlain);
|
5289
|
+
}
|
5290
|
+
MediumEditor.util.insertHTMLCommand(this.document, html);
|
5291
|
+
},
|
5292
|
+
|
5293
|
+
handlePasteBinPaste: function (event) {
|
5294
|
+
if (event.defaultPrevented) {
|
5295
|
+
this.removePasteBin();
|
5296
|
+
return;
|
5297
|
+
}
|
5298
|
+
|
5299
|
+
var clipboardContent = getClipboardContent(event, this.window, this.document),
|
5300
|
+
pastedHTML = clipboardContent['text/html'],
|
5301
|
+
pastedPlain = clipboardContent['text/plain'],
|
5302
|
+
editable = keyboardPasteEditable;
|
5303
|
+
|
5304
|
+
// If we have valid html already, or we're not in cleanPastedHTML mode
|
5305
|
+
// we can ignore the paste bin and just paste now
|
5306
|
+
if (!this.cleanPastedHTML || pastedHTML) {
|
5307
|
+
event.preventDefault();
|
5308
|
+
this.removePasteBin();
|
5309
|
+
this.doPaste(pastedHTML, pastedPlain, editable);
|
5310
|
+
|
5311
|
+
// The event handling code listens for paste on the editable element
|
5312
|
+
// in order to trigger the editablePaste event. Since this paste event
|
5313
|
+
// is happening on the pastebin, the event handling code never knows about it
|
5314
|
+
// So, we have to trigger editablePaste manually
|
5315
|
+
this.trigger('editablePaste', { currentTarget: editable, target: editable }, editable);
|
5316
|
+
return;
|
5317
|
+
}
|
5318
|
+
|
5319
|
+
// We need to look at the paste bin, so do a setTimeout to let the paste
|
5320
|
+
// fall through into the paste bin
|
5321
|
+
setTimeout(function () {
|
5322
|
+
// Only look for HTML if we're in cleanPastedHTML mode
|
5323
|
+
if (this.cleanPastedHTML) {
|
5324
|
+
// If clipboard didn't have HTML, try the paste bin
|
5325
|
+
pastedHTML = this.getPasteBinHtml();
|
5326
|
+
}
|
5327
|
+
|
5328
|
+
// If we needed the paste bin, we're done with it now, remove it
|
5329
|
+
this.removePasteBin();
|
5330
|
+
|
5331
|
+
// Handle the paste with the html from the paste bin
|
5332
|
+
this.doPaste(pastedHTML, pastedPlain, editable);
|
5333
|
+
|
5334
|
+
// The event handling code listens for paste on the editable element
|
5335
|
+
// in order to trigger the editablePaste event. Since this paste event
|
5336
|
+
// is happening on the pastebin, the event handling code never knows about it
|
5337
|
+
// So, we have to trigger editablePaste manually
|
5338
|
+
this.trigger('editablePaste', { currentTarget: editable, target: editable }, editable);
|
5339
|
+
}.bind(this), 0);
|
5340
|
+
},
|
5341
|
+
|
5342
|
+
handleKeydown: function (event, editable) {
|
5343
|
+
// if it's not Ctrl+V, do nothing
|
5344
|
+
if (!(MediumEditor.util.isKey(event, MediumEditor.util.keyCode.V) && MediumEditor.util.isMetaCtrlKey(event))) {
|
5345
|
+
return;
|
5346
|
+
}
|
5347
|
+
|
5348
|
+
event.stopImmediatePropagation();
|
5349
|
+
|
5350
|
+
this.removePasteBin();
|
5351
|
+
this.createPasteBin(editable);
|
5352
|
+
},
|
5353
|
+
|
5354
|
+
createPasteBin: function (editable) {
|
5355
|
+
var rects,
|
5356
|
+
range = MediumEditor.selection.getSelectionRange(this.document),
|
5357
|
+
top = this.window.pageYOffset;
|
5358
|
+
|
5359
|
+
keyboardPasteEditable = editable;
|
5360
|
+
|
5361
|
+
if (range) {
|
5362
|
+
rects = range.getClientRects();
|
5363
|
+
|
5364
|
+
// on empty line, rects is empty so we grab information from the first container of the range
|
5365
|
+
if (rects.length) {
|
5366
|
+
top += rects[0].top;
|
5367
|
+
} else {
|
5368
|
+
top += range.startContainer.getBoundingClientRect().top;
|
5369
|
+
}
|
5370
|
+
}
|
5371
|
+
|
5372
|
+
lastRange = range;
|
5373
|
+
|
5374
|
+
var pasteBinElm = this.document.createElement('div');
|
5375
|
+
pasteBinElm.id = this.pasteBinId = 'medium-editor-pastebin-' + (+Date.now());
|
5376
|
+
pasteBinElm.setAttribute('style', 'border: 1px red solid; position: absolute; top: ' + top + 'px; width: 10px; height: 10px; overflow: hidden; opacity: 0');
|
5377
|
+
pasteBinElm.setAttribute('contentEditable', true);
|
5378
|
+
pasteBinElm.innerHTML = pasteBinDefaultContent;
|
5379
|
+
|
5380
|
+
this.document.body.appendChild(pasteBinElm);
|
5381
|
+
|
5382
|
+
// avoid .focus() to stop other event (actually the paste event)
|
5383
|
+
this.on(pasteBinElm, 'focus', stopProp);
|
5384
|
+
this.on(pasteBinElm, 'focusin', stopProp);
|
5385
|
+
this.on(pasteBinElm, 'focusout', stopProp);
|
5386
|
+
|
5387
|
+
pasteBinElm.focus();
|
5388
|
+
|
5389
|
+
MediumEditor.selection.selectNode(pasteBinElm, this.document);
|
5390
|
+
|
5391
|
+
if (!this.boundHandlePaste) {
|
5392
|
+
this.boundHandlePaste = this.handlePasteBinPaste.bind(this);
|
5393
|
+
}
|
5394
|
+
|
5395
|
+
this.on(pasteBinElm, 'paste', this.boundHandlePaste);
|
5396
|
+
},
|
5397
|
+
|
5398
|
+
removePasteBin: function () {
|
5399
|
+
if (null !== lastRange) {
|
5400
|
+
MediumEditor.selection.selectRange(this.document, lastRange);
|
5401
|
+
lastRange = null;
|
5402
|
+
}
|
5403
|
+
|
5404
|
+
if (null !== keyboardPasteEditable) {
|
5405
|
+
keyboardPasteEditable = null;
|
5406
|
+
}
|
5407
|
+
|
5408
|
+
var pasteBinElm = this.getPasteBin();
|
5409
|
+
if (!pasteBinElm) {
|
5410
|
+
return;
|
5411
|
+
}
|
5412
|
+
|
5413
|
+
if (pasteBinElm) {
|
5414
|
+
this.off(pasteBinElm, 'focus', stopProp);
|
5415
|
+
this.off(pasteBinElm, 'focusin', stopProp);
|
5416
|
+
this.off(pasteBinElm, 'focusout', stopProp);
|
5417
|
+
this.off(pasteBinElm, 'paste', this.boundHandlePaste);
|
5418
|
+
pasteBinElm.parentElement.removeChild(pasteBinElm);
|
5058
5419
|
}
|
5059
5420
|
},
|
5060
5421
|
|
5422
|
+
getPasteBin: function () {
|
5423
|
+
return this.document.getElementById(this.pasteBinId);
|
5424
|
+
},
|
5425
|
+
|
5426
|
+
getPasteBinHtml: function () {
|
5427
|
+
var pasteBinElm = this.getPasteBin();
|
5428
|
+
|
5429
|
+
if (!pasteBinElm) {
|
5430
|
+
return false;
|
5431
|
+
}
|
5432
|
+
|
5433
|
+
// WebKit has a nice bug where it clones the paste bin if you paste from for example notepad
|
5434
|
+
// so we need to force plain text mode in this case
|
5435
|
+
if (pasteBinElm.firstChild && pasteBinElm.firstChild.id === 'mcepastebin') {
|
5436
|
+
return false;
|
5437
|
+
}
|
5438
|
+
|
5439
|
+
var pasteBinHtml = pasteBinElm.innerHTML;
|
5440
|
+
|
5441
|
+
// If paste bin is empty try using plain text mode
|
5442
|
+
// since that is better than nothing right
|
5443
|
+
if (!pasteBinHtml || pasteBinHtml === pasteBinDefaultContent) {
|
5444
|
+
return false;
|
5445
|
+
}
|
5446
|
+
|
5447
|
+
return pasteBinHtml;
|
5448
|
+
},
|
5449
|
+
|
5061
5450
|
cleanPaste: function (text) {
|
5062
5451
|
var i, elList, tmp, workEl,
|
5063
5452
|
multiline = /<p|<br|<div/.test(text),
|
@@ -5107,7 +5496,8 @@ MediumEditor.extensions = {};
|
|
5107
5496
|
pasteHTML: function (html, options) {
|
5108
5497
|
options = MediumEditor.util.defaults({}, options, {
|
5109
5498
|
cleanAttrs: this.cleanAttrs,
|
5110
|
-
cleanTags: this.cleanTags
|
5499
|
+
cleanTags: this.cleanTags,
|
5500
|
+
unwrapTags: this.unwrapTags
|
5111
5501
|
});
|
5112
5502
|
|
5113
5503
|
var elList, workEl, i, fragmentBody, pasteBlock = this.document.createDocumentFragment();
|
@@ -5129,21 +5519,25 @@ MediumEditor.extensions = {};
|
|
5129
5519
|
|
5130
5520
|
MediumEditor.util.cleanupAttrs(workEl, options.cleanAttrs);
|
5131
5521
|
MediumEditor.util.cleanupTags(workEl, options.cleanTags);
|
5522
|
+
MediumEditor.util.unwrapTags(workEl, options.unwrapTags);
|
5132
5523
|
}
|
5133
5524
|
|
5134
5525
|
MediumEditor.util.insertHTMLCommand(this.document, fragmentBody.innerHTML.replace(/ /g, ' '));
|
5135
5526
|
},
|
5136
5527
|
|
5528
|
+
// TODO (6.0): Make this an internal helper instead of member of paste handler
|
5137
5529
|
isCommonBlock: function (el) {
|
5138
5530
|
return (el && (el.nodeName.toLowerCase() === 'p' || el.nodeName.toLowerCase() === 'div'));
|
5139
5531
|
},
|
5140
5532
|
|
5533
|
+
// TODO (6.0): Make this an internal helper instead of member of paste handler
|
5141
5534
|
filterCommonBlocks: function (el) {
|
5142
5535
|
if (/^\s*$/.test(el.textContent) && el.parentNode) {
|
5143
5536
|
el.parentNode.removeChild(el);
|
5144
5537
|
}
|
5145
5538
|
},
|
5146
5539
|
|
5540
|
+
// TODO (6.0): Make this an internal helper instead of member of paste handler
|
5147
5541
|
filterLineBreak: function (el) {
|
5148
5542
|
if (this.isCommonBlock(el.previousElementSibling)) {
|
5149
5543
|
// remove stray br's following common block elements
|
@@ -5157,6 +5551,7 @@ MediumEditor.extensions = {};
|
|
5157
5551
|
}
|
5158
5552
|
},
|
5159
5553
|
|
5554
|
+
// TODO (6.0): Make this an internal helper instead of member of paste handler
|
5160
5555
|
// remove an element, including its parent, if it is the only element within its parent
|
5161
5556
|
removeWithParent: function (el) {
|
5162
5557
|
if (el && el.parentNode) {
|
@@ -5168,6 +5563,7 @@ MediumEditor.extensions = {};
|
|
5168
5563
|
}
|
5169
5564
|
},
|
5170
5565
|
|
5566
|
+
// TODO (6.0): Make this an internal helper instead of member of paste handler
|
5171
5567
|
cleanupSpans: function (containerEl) {
|
5172
5568
|
var i,
|
5173
5569
|
el,
|
@@ -5234,37 +5630,61 @@ MediumEditor.extensions = {};
|
|
5234
5630
|
},
|
5235
5631
|
|
5236
5632
|
initPlaceholders: function () {
|
5237
|
-
this.getEditorElements().forEach(
|
5238
|
-
|
5239
|
-
|
5240
|
-
|
5241
|
-
|
5242
|
-
|
5633
|
+
this.getEditorElements().forEach(this.initElement, this);
|
5634
|
+
},
|
5635
|
+
|
5636
|
+
handleAddElement: function (event, editable) {
|
5637
|
+
this.initElement(editable);
|
5638
|
+
},
|
5639
|
+
|
5640
|
+
initElement: function (el) {
|
5641
|
+
if (!el.getAttribute('data-placeholder')) {
|
5642
|
+
el.setAttribute('data-placeholder', this.text);
|
5643
|
+
}
|
5644
|
+
this.updatePlaceholder(el);
|
5243
5645
|
},
|
5244
5646
|
|
5245
5647
|
destroy: function () {
|
5246
|
-
this.getEditorElements().forEach(
|
5247
|
-
|
5248
|
-
|
5249
|
-
|
5250
|
-
|
5648
|
+
this.getEditorElements().forEach(this.cleanupElement, this);
|
5649
|
+
},
|
5650
|
+
|
5651
|
+
handleRemoveElement: function (event, editable) {
|
5652
|
+
this.cleanupElement(editable);
|
5653
|
+
},
|
5654
|
+
|
5655
|
+
cleanupElement: function (el) {
|
5656
|
+
if (el.getAttribute('data-placeholder') === this.text) {
|
5657
|
+
el.removeAttribute('data-placeholder');
|
5658
|
+
}
|
5251
5659
|
},
|
5252
5660
|
|
5253
5661
|
showPlaceholder: function (el) {
|
5254
5662
|
if (el) {
|
5255
|
-
|
5663
|
+
// https://github.com/yabwe/medium-editor/issues/234
|
5664
|
+
// In firefox, styling the placeholder with an absolutely positioned
|
5665
|
+
// pseudo element causes the cursor to appear in a bad location
|
5666
|
+
// when the element is completely empty, so apply a different class to
|
5667
|
+
// style it with a relatively positioned pseudo element
|
5668
|
+
if (MediumEditor.util.isFF && el.childNodes.length === 0) {
|
5669
|
+
el.classList.add('medium-editor-placeholder-relative');
|
5670
|
+
el.classList.remove('medium-editor-placeholder');
|
5671
|
+
} else {
|
5672
|
+
el.classList.add('medium-editor-placeholder');
|
5673
|
+
el.classList.remove('medium-editor-placeholder-relative');
|
5674
|
+
}
|
5256
5675
|
}
|
5257
5676
|
},
|
5258
5677
|
|
5259
5678
|
hidePlaceholder: function (el) {
|
5260
5679
|
if (el) {
|
5261
5680
|
el.classList.remove('medium-editor-placeholder');
|
5681
|
+
el.classList.remove('medium-editor-placeholder-relative');
|
5262
5682
|
}
|
5263
5683
|
},
|
5264
5684
|
|
5265
5685
|
updatePlaceholder: function (el, dontShow) {
|
5266
5686
|
// If the element has content, hide the placeholder
|
5267
|
-
if (el.querySelector('img, blockquote, ul, ol') || (el.textContent.replace(/^\s+|\s+$/g, '') !== '')) {
|
5687
|
+
if (el.querySelector('img, blockquote, ul, ol, table') || (el.textContent.replace(/^\s+|\s+$/g, '') !== '')) {
|
5268
5688
|
return this.hidePlaceholder(el);
|
5269
5689
|
}
|
5270
5690
|
|
@@ -5284,6 +5704,10 @@ MediumEditor.extensions = {};
|
|
5284
5704
|
|
5285
5705
|
// When the editor loses focus, check if the placeholder should be visible
|
5286
5706
|
this.subscribe('blur', this.handleBlur.bind(this));
|
5707
|
+
|
5708
|
+
// Need to know when elements are added/removed from the editor
|
5709
|
+
this.subscribe('addElement', this.handleAddElement.bind(this));
|
5710
|
+
this.subscribe('removeElement', this.handleRemoveElement.bind(this));
|
5287
5711
|
},
|
5288
5712
|
|
5289
5713
|
handleInput: function (event, element) {
|
@@ -5500,6 +5924,10 @@ MediumEditor.extensions = {};
|
|
5500
5924
|
|
5501
5925
|
// Toolbar accessors
|
5502
5926
|
|
5927
|
+
getInteractionElements: function () {
|
5928
|
+
return this.getToolbarElement();
|
5929
|
+
},
|
5930
|
+
|
5503
5931
|
getToolbarElement: function () {
|
5504
5932
|
if (!this.toolbar) {
|
5505
5933
|
this.toolbar = this.createToolbar();
|
@@ -5825,30 +6253,26 @@ MediumEditor.extensions = {};
|
|
5825
6253
|
|
5826
6254
|
setToolbarPosition: function () {
|
5827
6255
|
var container = this.base.getFocusedElement(),
|
5828
|
-
selection = this.window.getSelection()
|
5829
|
-
anchorPreview;
|
6256
|
+
selection = this.window.getSelection();
|
5830
6257
|
|
5831
6258
|
// If there isn't a valid selection, bail
|
5832
6259
|
if (!container) {
|
5833
6260
|
return this;
|
5834
6261
|
}
|
5835
6262
|
|
5836
|
-
if (this.static
|
5837
|
-
this.showToolbar();
|
5838
|
-
this.positionStaticToolbar(container);
|
5839
|
-
} else if (!selection.isCollapsed) {
|
6263
|
+
if (this.static || !selection.isCollapsed) {
|
5840
6264
|
this.showToolbar();
|
5841
6265
|
|
5842
6266
|
// we don't need any absolute positioning if relativeContainer is set
|
5843
6267
|
if (!this.relativeContainer) {
|
5844
|
-
this.
|
6268
|
+
if (this.static) {
|
6269
|
+
this.positionStaticToolbar(container);
|
6270
|
+
} else {
|
6271
|
+
this.positionToolbar(selection);
|
6272
|
+
}
|
5845
6273
|
}
|
5846
|
-
}
|
5847
|
-
|
5848
|
-
anchorPreview = this.base.getExtensionByName('anchor-preview');
|
5849
6274
|
|
5850
|
-
|
5851
|
-
anchorPreview.hidePreview();
|
6275
|
+
this.trigger('positionedToolbar', {}, this.base.getFocusedElement());
|
5852
6276
|
}
|
5853
6277
|
},
|
5854
6278
|
|
@@ -5927,35 +6351,66 @@ MediumEditor.extensions = {};
|
|
5927
6351
|
}
|
5928
6352
|
}
|
5929
6353
|
|
5930
|
-
var
|
5931
|
-
middleBoundary = (boundary.left + boundary.right) / 2,
|
6354
|
+
var containerWidth = this.window.innerWidth,
|
5932
6355
|
toolbarElement = this.getToolbarElement(),
|
5933
6356
|
toolbarHeight = toolbarElement.offsetHeight,
|
5934
6357
|
toolbarWidth = toolbarElement.offsetWidth,
|
5935
6358
|
halfOffsetWidth = toolbarWidth / 2,
|
5936
6359
|
buttonHeight = 50,
|
5937
|
-
defaultLeft = this.diffLeft - halfOffsetWidth
|
6360
|
+
defaultLeft = this.diffLeft - halfOffsetWidth,
|
6361
|
+
elementsContainer = this.getEditorOption('elementsContainer'),
|
6362
|
+
elementsContainerAbsolute = ['absolute', 'fixed'].indexOf(window.getComputedStyle(elementsContainer).getPropertyValue('position')) > -1,
|
6363
|
+
positions = {},
|
6364
|
+
relativeBoundary = {},
|
6365
|
+
middleBoundary, elementsContainerBoundary;
|
6366
|
+
|
6367
|
+
// If container element is absolute / fixed, recalculate boundaries to be relative to the container
|
6368
|
+
if (elementsContainerAbsolute) {
|
6369
|
+
elementsContainerBoundary = elementsContainer.getBoundingClientRect();
|
6370
|
+
['top', 'left'].forEach(function (key) {
|
6371
|
+
relativeBoundary[key] = boundary[key] - elementsContainerBoundary[key];
|
6372
|
+
});
|
6373
|
+
|
6374
|
+
relativeBoundary.width = boundary.width;
|
6375
|
+
relativeBoundary.height = boundary.height;
|
6376
|
+
boundary = relativeBoundary;
|
6377
|
+
|
6378
|
+
containerWidth = elementsContainerBoundary.width;
|
6379
|
+
|
6380
|
+
// Adjust top position according to container scroll position
|
6381
|
+
positions.top = elementsContainer.scrollTop;
|
6382
|
+
} else {
|
6383
|
+
// Adjust top position according to window scroll position
|
6384
|
+
positions.top = this.window.pageYOffset;
|
6385
|
+
}
|
6386
|
+
|
6387
|
+
middleBoundary = boundary.left + boundary.width / 2;
|
6388
|
+
positions.top += boundary.top - toolbarHeight;
|
5938
6389
|
|
5939
6390
|
if (boundary.top < buttonHeight) {
|
5940
6391
|
toolbarElement.classList.add('medium-toolbar-arrow-over');
|
5941
6392
|
toolbarElement.classList.remove('medium-toolbar-arrow-under');
|
5942
|
-
|
6393
|
+
positions.top += buttonHeight + boundary.height - this.diffTop;
|
5943
6394
|
} else {
|
5944
6395
|
toolbarElement.classList.add('medium-toolbar-arrow-under');
|
5945
6396
|
toolbarElement.classList.remove('medium-toolbar-arrow-over');
|
5946
|
-
|
6397
|
+
positions.top += this.diffTop;
|
5947
6398
|
}
|
5948
6399
|
|
5949
6400
|
if (middleBoundary < halfOffsetWidth) {
|
5950
|
-
|
5951
|
-
|
5952
|
-
} else if ((
|
5953
|
-
|
5954
|
-
|
6401
|
+
positions.left = defaultLeft + halfOffsetWidth;
|
6402
|
+
positions.right = 'initial';
|
6403
|
+
} else if ((containerWidth - middleBoundary) < halfOffsetWidth) {
|
6404
|
+
positions.left = 'auto';
|
6405
|
+
positions.right = 0;
|
5955
6406
|
} else {
|
5956
|
-
|
5957
|
-
|
6407
|
+
positions.left = defaultLeft + middleBoundary;
|
6408
|
+
positions.right = 'initial';
|
5958
6409
|
}
|
6410
|
+
|
6411
|
+
['top', 'left', 'right'].forEach(function (key) {
|
6412
|
+
toolbarElement.style[key] = positions[key] + (isNaN(positions[key]) ? '' : 'px');
|
6413
|
+
});
|
5959
6414
|
}
|
5960
6415
|
});
|
5961
6416
|
|
@@ -6162,6 +6617,31 @@ MediumEditor.extensions = {};
|
|
6162
6617
|
// then pressing backspace key should change the <blockquote> to a <p> tag
|
6163
6618
|
event.preventDefault();
|
6164
6619
|
MediumEditor.util.execFormatBlock(this.options.ownerDocument, 'p');
|
6620
|
+
} else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER) &&
|
6621
|
+
(MediumEditor.util.getClosestTag(node, 'blockquote') !== false) &&
|
6622
|
+
MediumEditor.selection.getCaretOffsets(node).right === 0) {
|
6623
|
+
|
6624
|
+
// when cursor is at the end of <blockquote>,
|
6625
|
+
// then pressing enter key should create <p> tag, not <blockquote>
|
6626
|
+
p = this.options.ownerDocument.createElement('p');
|
6627
|
+
p.innerHTML = '<br>';
|
6628
|
+
node.parentElement.insertBefore(p, node.nextSibling);
|
6629
|
+
|
6630
|
+
// move the cursor into the new paragraph
|
6631
|
+
MediumEditor.selection.moveCursor(this.options.ownerDocument, p);
|
6632
|
+
|
6633
|
+
event.preventDefault();
|
6634
|
+
} else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.BACKSPACE) &&
|
6635
|
+
MediumEditor.util.isMediumEditorElement(node.parentElement) &&
|
6636
|
+
!node.previousElementSibling &&
|
6637
|
+
node.nextElementSibling &&
|
6638
|
+
isEmpty.test(node.innerHTML)) {
|
6639
|
+
|
6640
|
+
// when cursor is in the first element, it's empty and user presses backspace,
|
6641
|
+
// do delete action instead to get rid of the first element and move caret to 2nd
|
6642
|
+
event.preventDefault();
|
6643
|
+
MediumEditor.selection.moveCursor(this.options.ownerDocument, node.nextSibling);
|
6644
|
+
node.parentElement.removeChild(node);
|
6165
6645
|
}
|
6166
6646
|
}
|
6167
6647
|
|
@@ -6173,7 +6653,9 @@ MediumEditor.extensions = {};
|
|
6173
6653
|
return;
|
6174
6654
|
}
|
6175
6655
|
|
6176
|
-
|
6656
|
+
// https://github.com/yabwe/medium-editor/issues/994
|
6657
|
+
// Firefox thrown an error when calling `formatBlock` on an empty editable blockContainer that's not a <div>
|
6658
|
+
if (MediumEditor.util.isMediumEditorElement(node) && node.children.length === 0 && !MediumEditor.util.isBlockContainer(node)) {
|
6177
6659
|
this.options.ownerDocument.execCommand('formatBlock', false, 'p');
|
6178
6660
|
}
|
6179
6661
|
|
@@ -6194,6 +6676,13 @@ MediumEditor.extensions = {};
|
|
6194
6676
|
}
|
6195
6677
|
}
|
6196
6678
|
|
6679
|
+
function handleEditableInput(event, editable) {
|
6680
|
+
var textarea = editable.parentNode.querySelector('textarea[medium-editor-textarea-id="' + editable.getAttribute('medium-editor-textarea-id') + '"]');
|
6681
|
+
if (textarea) {
|
6682
|
+
textarea.value = editable.innerHTML.trim();
|
6683
|
+
}
|
6684
|
+
}
|
6685
|
+
|
6197
6686
|
// Internal helper methods which shouldn't be exposed externally
|
6198
6687
|
|
6199
6688
|
function addToEditors(win) {
|
@@ -6227,30 +6716,50 @@ MediumEditor.extensions = {};
|
|
6227
6716
|
win._mediumEditors[this.id] = null;
|
6228
6717
|
}
|
6229
6718
|
|
6230
|
-
function createElementsArray(selector) {
|
6719
|
+
function createElementsArray(selector, doc, filterEditorElements) {
|
6720
|
+
var elements = [];
|
6721
|
+
|
6231
6722
|
if (!selector) {
|
6232
6723
|
selector = [];
|
6233
6724
|
}
|
6234
6725
|
// If string, use as query selector
|
6235
6726
|
if (typeof selector === 'string') {
|
6236
|
-
selector =
|
6727
|
+
selector = doc.querySelectorAll(selector);
|
6237
6728
|
}
|
6238
6729
|
// If element, put into array
|
6239
6730
|
if (MediumEditor.util.isElement(selector)) {
|
6240
6731
|
selector = [selector];
|
6241
6732
|
}
|
6242
|
-
// Convert NodeList (or other array like object) into an array
|
6243
|
-
var elements = Array.prototype.slice.apply(selector);
|
6244
6733
|
|
6245
|
-
|
6246
|
-
|
6247
|
-
|
6248
|
-
|
6249
|
-
|
6250
|
-
|
6251
|
-
|
6734
|
+
if (filterEditorElements) {
|
6735
|
+
// Remove elements that have already been initialized by the editor
|
6736
|
+
// selecotr might not be an array (ie NodeList) so use for loop
|
6737
|
+
for (var i = 0; i < selector.length; i++) {
|
6738
|
+
var el = selector[i];
|
6739
|
+
if (MediumEditor.util.isElement(el) &&
|
6740
|
+
!el.getAttribute('data-medium-editor-element') &&
|
6741
|
+
!el.getAttribute('medium-editor-textarea-id')) {
|
6742
|
+
elements.push(el);
|
6743
|
+
}
|
6252
6744
|
}
|
6253
|
-
}
|
6745
|
+
} else {
|
6746
|
+
// Convert NodeList (or other array like object) into an array
|
6747
|
+
elements = Array.prototype.slice.apply(selector);
|
6748
|
+
}
|
6749
|
+
|
6750
|
+
return elements;
|
6751
|
+
}
|
6752
|
+
|
6753
|
+
function cleanupTextareaElement(element) {
|
6754
|
+
var textarea = element.parentNode.querySelector('textarea[medium-editor-textarea-id="' + element.getAttribute('medium-editor-textarea-id') + '"]');
|
6755
|
+
if (textarea) {
|
6756
|
+
// Un-hide the textarea
|
6757
|
+
textarea.classList.remove('medium-editor-hidden');
|
6758
|
+
textarea.removeAttribute('medium-editor-textarea-id');
|
6759
|
+
}
|
6760
|
+
if (element.parentNode) {
|
6761
|
+
element.parentNode.removeChild(element);
|
6762
|
+
}
|
6254
6763
|
}
|
6255
6764
|
|
6256
6765
|
function setExtensionDefaults(extension, defaults) {
|
@@ -6328,17 +6837,17 @@ MediumEditor.extensions = {};
|
|
6328
6837
|
return !this.options.extensions['imageDragging'];
|
6329
6838
|
}
|
6330
6839
|
|
6331
|
-
function createContentEditable(textarea
|
6840
|
+
function createContentEditable(textarea) {
|
6332
6841
|
var div = this.options.ownerDocument.createElement('div'),
|
6333
6842
|
now = Date.now(),
|
6334
|
-
uniqueId = 'medium-editor-' + now
|
6843
|
+
uniqueId = 'medium-editor-' + now,
|
6335
6844
|
atts = textarea.attributes;
|
6336
6845
|
|
6337
6846
|
// Some browsers can move pretty fast, since we're using a timestamp
|
6338
6847
|
// to make a unique-id, ensure that the id is actually unique on the page
|
6339
6848
|
while (this.options.ownerDocument.getElementById(uniqueId)) {
|
6340
6849
|
now++;
|
6341
|
-
uniqueId = 'medium-editor-' + now
|
6850
|
+
uniqueId = 'medium-editor-' + now;
|
6342
6851
|
}
|
6343
6852
|
|
6344
6853
|
div.className = textarea.className;
|
@@ -6355,6 +6864,16 @@ MediumEditor.extensions = {};
|
|
6355
6864
|
}
|
6356
6865
|
}
|
6357
6866
|
|
6867
|
+
// If textarea has a form, listen for reset on the form to clear
|
6868
|
+
// the content of the created div
|
6869
|
+
if (textarea.form) {
|
6870
|
+
this.on(textarea.form, 'reset', function (event) {
|
6871
|
+
if (!event.defaultPrevented) {
|
6872
|
+
this.resetContent(this.options.ownerDocument.getElementById(uniqueId));
|
6873
|
+
}
|
6874
|
+
}.bind(this));
|
6875
|
+
}
|
6876
|
+
|
6358
6877
|
textarea.classList.add('medium-editor-hidden');
|
6359
6878
|
textarea.parentNode.insertBefore(
|
6360
6879
|
div,
|
@@ -6364,37 +6883,57 @@ MediumEditor.extensions = {};
|
|
6364
6883
|
return div;
|
6365
6884
|
}
|
6366
6885
|
|
6367
|
-
function
|
6368
|
-
|
6886
|
+
function initElement(element, editorId) {
|
6887
|
+
if (!element.getAttribute('data-medium-editor-element')) {
|
6888
|
+
if (element.nodeName.toLowerCase() === 'textarea') {
|
6889
|
+
element = createContentEditable.call(this, element);
|
6890
|
+
|
6891
|
+
// Make sure we only attach to editableInput once for <textarea> elements
|
6892
|
+
if (!this.instanceHandleEditableInput) {
|
6893
|
+
this.instanceHandleEditableInput = handleEditableInput.bind(this);
|
6894
|
+
this.subscribe('editableInput', this.instanceHandleEditableInput);
|
6895
|
+
}
|
6896
|
+
}
|
6369
6897
|
|
6370
|
-
this.elements.forEach(function (element, index) {
|
6371
6898
|
if (!this.options.disableEditing && !element.getAttribute('data-disable-editing')) {
|
6372
6899
|
element.setAttribute('contentEditable', true);
|
6373
6900
|
element.setAttribute('spellcheck', this.options.spellcheck);
|
6374
6901
|
}
|
6375
|
-
element.setAttribute('data-medium-editor-element', true);
|
6376
|
-
element.setAttribute('role', 'textbox');
|
6377
|
-
element.setAttribute('aria-multiline', true);
|
6378
|
-
element.setAttribute('medium-editor-index', index);
|
6379
6902
|
|
6380
|
-
|
6381
|
-
|
6903
|
+
// Make sure we only attach to editableKeydownEnter once for disable-return options
|
6904
|
+
if (!this.instanceHandleEditableKeydownEnter) {
|
6905
|
+
if (element.getAttribute('data-disable-return') || element.getAttribute('data-disable-double-return')) {
|
6906
|
+
this.instanceHandleEditableKeydownEnter = handleDisabledEnterKeydown.bind(this);
|
6907
|
+
this.subscribe('editableKeydownEnter', this.instanceHandleEditableKeydownEnter);
|
6908
|
+
}
|
6382
6909
|
}
|
6383
|
-
}, this);
|
6384
6910
|
|
6385
|
-
|
6386
|
-
|
6387
|
-
|
6388
|
-
|
6389
|
-
|
6390
|
-
|
6391
|
-
|
6911
|
+
// if we're not disabling return, add a handler to help handle cleanup
|
6912
|
+
// for certain cases when enter is pressed
|
6913
|
+
if (!this.options.disableReturn && !element.getAttribute('data-disable-return')) {
|
6914
|
+
this.on(element, 'keyup', handleKeyup.bind(this));
|
6915
|
+
}
|
6916
|
+
|
6917
|
+
var elementId = MediumEditor.util.guid();
|
6918
|
+
|
6919
|
+
element.setAttribute('data-medium-editor-element', true);
|
6920
|
+
element.classList.add('medium-editor-element');
|
6921
|
+
element.setAttribute('role', 'textbox');
|
6922
|
+
element.setAttribute('aria-multiline', true);
|
6923
|
+
element.setAttribute('data-medium-editor-editor-index', editorId);
|
6924
|
+
// TODO: Merge data-medium-editor-element and medium-editor-index attributes for 6.0.0
|
6925
|
+
// medium-editor-index is not named correctly anymore and can be re-purposed to signify
|
6926
|
+
// whether the element has been initialized or not
|
6927
|
+
element.setAttribute('medium-editor-index', elementId);
|
6928
|
+
initialContent[elementId] = element.innerHTML;
|
6929
|
+
|
6930
|
+
this.events.attachAllEventsToElement(element);
|
6392
6931
|
}
|
6932
|
+
|
6933
|
+
return element;
|
6393
6934
|
}
|
6394
6935
|
|
6395
6936
|
function attachHandlers() {
|
6396
|
-
var i;
|
6397
|
-
|
6398
6937
|
// attach to tabs
|
6399
6938
|
this.subscribe('editableKeydownTab', handleTabKeydown.bind(this));
|
6400
6939
|
|
@@ -6407,27 +6946,14 @@ MediumEditor.extensions = {};
|
|
6407
6946
|
this.subscribe('editableKeydownSpace', handleDisableExtraSpaces.bind(this));
|
6408
6947
|
}
|
6409
6948
|
|
6410
|
-
//
|
6411
|
-
if (this.
|
6412
|
-
|
6413
|
-
|
6414
|
-
|
6415
|
-
|
6416
|
-
this.subscribe('editableKeydownEnter', handleDisabledEnterKeydown.bind(this));
|
6417
|
-
break;
|
6418
|
-
}
|
6949
|
+
// Make sure we only attach to editableKeydownEnter once for disable-return options
|
6950
|
+
if (!this.instanceHandleEditableKeydownEnter) {
|
6951
|
+
// disabling return or double return
|
6952
|
+
if (this.options.disableReturn || this.options.disableDoubleReturn) {
|
6953
|
+
this.instanceHandleEditableKeydownEnter = handleDisabledEnterKeydown.bind(this);
|
6954
|
+
this.subscribe('editableKeydownEnter', this.instanceHandleEditableKeydownEnter);
|
6419
6955
|
}
|
6420
6956
|
}
|
6421
|
-
|
6422
|
-
// if we're not disabling return, add a handler to help handle cleanup
|
6423
|
-
// for certain cases when enter is pressed
|
6424
|
-
if (!this.options.disableReturn) {
|
6425
|
-
this.elements.forEach(function (element) {
|
6426
|
-
if (!element.getAttribute('data-disable-return')) {
|
6427
|
-
this.on(element, 'keyup', handleKeyup.bind(this));
|
6428
|
-
}
|
6429
|
-
}, this);
|
6430
|
-
}
|
6431
6957
|
}
|
6432
6958
|
|
6433
6959
|
function initExtensions() {
|
@@ -6519,7 +7045,8 @@ MediumEditor.extensions = {};
|
|
6519
7045
|
/*jslint regexp: true*/
|
6520
7046
|
var appendAction = /^append-(.+)$/gi,
|
6521
7047
|
justifyAction = /justify([A-Za-z]*)$/g, /* Detecting if is justifyCenter|Right|Left */
|
6522
|
-
match
|
7048
|
+
match,
|
7049
|
+
cmdValueArgument;
|
6523
7050
|
/*jslint regexp: false*/
|
6524
7051
|
|
6525
7052
|
// Actions starting with 'append-' should attempt to format a block of text ('formatBlock') using a specific
|
@@ -6530,11 +7057,21 @@ MediumEditor.extensions = {};
|
|
6530
7057
|
}
|
6531
7058
|
|
6532
7059
|
if (action === 'fontSize') {
|
6533
|
-
|
7060
|
+
// TODO: Deprecate support for opts.size in 6.0.0
|
7061
|
+
if (opts.size) {
|
7062
|
+
MediumEditor.util.deprecated('.size option for fontSize command', '.value', '6.0.0');
|
7063
|
+
}
|
7064
|
+
cmdValueArgument = opts.value || opts.size;
|
7065
|
+
return this.options.ownerDocument.execCommand('fontSize', false, cmdValueArgument);
|
6534
7066
|
}
|
6535
7067
|
|
6536
7068
|
if (action === 'fontName') {
|
6537
|
-
|
7069
|
+
// TODO: Deprecate support for opts.name in 6.0.0
|
7070
|
+
if (opts.name) {
|
7071
|
+
MediumEditor.util.deprecated('.name option for fontName command', '.value', '6.0.0');
|
7072
|
+
}
|
7073
|
+
cmdValueArgument = opts.value || opts.name;
|
7074
|
+
return this.options.ownerDocument.execCommand('fontName', false, cmdValueArgument);
|
6538
7075
|
}
|
6539
7076
|
|
6540
7077
|
if (action === 'createLink') {
|
@@ -6558,7 +7095,8 @@ MediumEditor.extensions = {};
|
|
6558
7095
|
return result;
|
6559
7096
|
}
|
6560
7097
|
|
6561
|
-
|
7098
|
+
cmdValueArgument = opts && opts.value;
|
7099
|
+
return this.options.ownerDocument.execCommand(action, false, cmdValueArgument);
|
6562
7100
|
}
|
6563
7101
|
|
6564
7102
|
/* If we've just justified text within a container block
|
@@ -6606,6 +7144,8 @@ MediumEditor.extensions = {};
|
|
6606
7144
|
}
|
6607
7145
|
}
|
6608
7146
|
|
7147
|
+
var initialContent = {};
|
7148
|
+
|
6609
7149
|
MediumEditor.prototype = {
|
6610
7150
|
// NOT DOCUMENTED - exposed for backwards compatability
|
6611
7151
|
init: function (elements, options) {
|
@@ -6624,19 +7164,19 @@ MediumEditor.extensions = {};
|
|
6624
7164
|
return;
|
6625
7165
|
}
|
6626
7166
|
|
6627
|
-
|
7167
|
+
addToEditors.call(this, this.options.contentWindow);
|
7168
|
+
this.events = new MediumEditor.Events(this);
|
7169
|
+
this.elements = [];
|
7170
|
+
|
7171
|
+
this.addElements(this.origElements);
|
6628
7172
|
|
6629
7173
|
if (this.elements.length === 0) {
|
6630
7174
|
return;
|
6631
7175
|
}
|
6632
7176
|
|
6633
7177
|
this.isActive = true;
|
6634
|
-
addToEditors.call(this, this.options.contentWindow);
|
6635
|
-
|
6636
|
-
this.events = new MediumEditor.Events(this);
|
6637
7178
|
|
6638
7179
|
// Call initialization helpers
|
6639
|
-
initElements.call(this);
|
6640
7180
|
initExtensions.call(this);
|
6641
7181
|
attachHandlers.call(this);
|
6642
7182
|
},
|
@@ -6666,45 +7206,52 @@ MediumEditor.extensions = {};
|
|
6666
7206
|
element.removeAttribute('contentEditable');
|
6667
7207
|
element.removeAttribute('spellcheck');
|
6668
7208
|
element.removeAttribute('data-medium-editor-element');
|
7209
|
+
element.classList.remove('medium-editor-element');
|
6669
7210
|
element.removeAttribute('role');
|
6670
7211
|
element.removeAttribute('aria-multiline');
|
6671
7212
|
element.removeAttribute('medium-editor-index');
|
7213
|
+
element.removeAttribute('data-medium-editor-editor-index');
|
6672
7214
|
|
6673
7215
|
// Remove any elements created for textareas
|
6674
|
-
if (element.
|
6675
|
-
|
6676
|
-
if (textarea) {
|
6677
|
-
// Un-hide the textarea
|
6678
|
-
textarea.classList.remove('medium-editor-hidden');
|
6679
|
-
}
|
6680
|
-
if (element.parentNode) {
|
6681
|
-
element.parentNode.removeChild(element);
|
6682
|
-
}
|
7216
|
+
if (element.getAttribute('medium-editor-textarea-id')) {
|
7217
|
+
cleanupTextareaElement(element);
|
6683
7218
|
}
|
6684
7219
|
}, this);
|
6685
7220
|
this.elements = [];
|
7221
|
+
this.instanceHandleEditableKeydownEnter = null;
|
7222
|
+
this.instanceHandleEditableInput = null;
|
6686
7223
|
|
6687
7224
|
removeFromEditors.call(this, this.options.contentWindow);
|
6688
7225
|
},
|
6689
7226
|
|
6690
7227
|
on: function (target, event, listener, useCapture) {
|
6691
7228
|
this.events.attachDOMEvent(target, event, listener, useCapture);
|
7229
|
+
|
7230
|
+
return this;
|
6692
7231
|
},
|
6693
7232
|
|
6694
7233
|
off: function (target, event, listener, useCapture) {
|
6695
7234
|
this.events.detachDOMEvent(target, event, listener, useCapture);
|
7235
|
+
|
7236
|
+
return this;
|
6696
7237
|
},
|
6697
7238
|
|
6698
7239
|
subscribe: function (event, listener) {
|
6699
7240
|
this.events.attachCustomEvent(event, listener);
|
7241
|
+
|
7242
|
+
return this;
|
6700
7243
|
},
|
6701
7244
|
|
6702
7245
|
unsubscribe: function (event, listener) {
|
6703
7246
|
this.events.detachCustomEvent(event, listener);
|
7247
|
+
|
7248
|
+
return this;
|
6704
7249
|
},
|
6705
7250
|
|
6706
7251
|
trigger: function (name, data, editable) {
|
6707
7252
|
this.events.triggerCustomEvent(name, data, editable);
|
7253
|
+
|
7254
|
+
return this;
|
6708
7255
|
},
|
6709
7256
|
|
6710
7257
|
delay: function (fn) {
|
@@ -6719,8 +7266,10 @@ MediumEditor.extensions = {};
|
|
6719
7266
|
serialize: function () {
|
6720
7267
|
var i,
|
6721
7268
|
elementid,
|
6722
|
-
content = {}
|
6723
|
-
|
7269
|
+
content = {},
|
7270
|
+
len = this.elements.length;
|
7271
|
+
|
7272
|
+
for (i = 0; i < len; i += 1) {
|
6724
7273
|
elementid = (this.elements[i].id !== '') ? this.elements[i].id : 'element-' + i;
|
6725
7274
|
content[elementid] = {
|
6726
7275
|
value: this.elements[i].innerHTML.trim()
|
@@ -6954,7 +7503,8 @@ MediumEditor.extensions = {};
|
|
6954
7503
|
|
6955
7504
|
createLink: function (opts) {
|
6956
7505
|
var currentEditor = MediumEditor.selection.getSelectionElement(this.options.contentWindow),
|
6957
|
-
customEvent = {}
|
7506
|
+
customEvent = {},
|
7507
|
+
targetUrl;
|
6958
7508
|
|
6959
7509
|
// Make sure the selection is within an element this editor is tracking
|
6960
7510
|
if (this.elements.indexOf(currentEditor) === -1) {
|
@@ -6963,7 +7513,12 @@ MediumEditor.extensions = {};
|
|
6963
7513
|
|
6964
7514
|
try {
|
6965
7515
|
this.events.disableCustomEvent('editableInput');
|
6966
|
-
|
7516
|
+
// TODO: Deprecate support for opts.url in 6.0.0
|
7517
|
+
if (opts.url) {
|
7518
|
+
MediumEditor.util.deprecated('.url option for createLink', '.value', '6.0.0');
|
7519
|
+
}
|
7520
|
+
targetUrl = opts.url || opts.value;
|
7521
|
+
if (targetUrl && targetUrl.trim().length > 0) {
|
6967
7522
|
var currentSelection = this.options.contentWindow.getSelection();
|
6968
7523
|
if (currentSelection) {
|
6969
7524
|
var currRange = currentSelection.getRangeAt(0),
|
@@ -7055,7 +7610,7 @@ MediumEditor.extensions = {};
|
|
7055
7610
|
}
|
7056
7611
|
|
7057
7612
|
// Creates the link in the document fragment
|
7058
|
-
MediumEditor.util.createLink(this.options.ownerDocument, textNodes,
|
7613
|
+
MediumEditor.util.createLink(this.options.ownerDocument, textNodes, targetUrl.trim());
|
7059
7614
|
|
7060
7615
|
// Chrome trims the leading whitespaces when inserting HTML, which messes up restoring the selection.
|
7061
7616
|
var leadingWhitespacesCount = (fragment.firstChild.innerHTML.match(/^\s+/) || [''])[0].length;
|
@@ -7067,13 +7622,13 @@ MediumEditor.extensions = {};
|
|
7067
7622
|
|
7068
7623
|
this.importSelection(exportedSelection);
|
7069
7624
|
} else {
|
7070
|
-
this.options.ownerDocument.execCommand('createLink', false,
|
7625
|
+
this.options.ownerDocument.execCommand('createLink', false, targetUrl);
|
7071
7626
|
}
|
7072
7627
|
|
7073
7628
|
if (this.options.targetBlank || opts.target === '_blank') {
|
7074
|
-
MediumEditor.util.setTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument),
|
7629
|
+
MediumEditor.util.setTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), targetUrl);
|
7075
7630
|
} else {
|
7076
|
-
MediumEditor.util.removeTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument),
|
7631
|
+
MediumEditor.util.removeTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), targetUrl);
|
7077
7632
|
}
|
7078
7633
|
|
7079
7634
|
if (opts.buttonClass) {
|
@@ -7085,7 +7640,7 @@ MediumEditor.extensions = {};
|
|
7085
7640
|
if (this.options.targetBlank || opts.target === '_blank' || opts.buttonClass) {
|
7086
7641
|
customEvent = this.options.ownerDocument.createEvent('HTMLEvents');
|
7087
7642
|
customEvent.initEvent('input', true, true, this.options.contentWindow);
|
7088
|
-
for (var i = 0
|
7643
|
+
for (var i = 0, len = this.elements.length; i < len; i += 1) {
|
7089
7644
|
this.elements[i].dispatchEvent(customEvent);
|
7090
7645
|
}
|
7091
7646
|
}
|
@@ -7110,9 +7665,98 @@ MediumEditor.extensions = {};
|
|
7110
7665
|
if (this.elements[index]) {
|
7111
7666
|
var target = this.elements[index];
|
7112
7667
|
target.innerHTML = html;
|
7113
|
-
this.
|
7668
|
+
this.checkContentChanged(target);
|
7669
|
+
}
|
7670
|
+
},
|
7671
|
+
|
7672
|
+
getContent: function (index) {
|
7673
|
+
index = index || 0;
|
7674
|
+
|
7675
|
+
if (this.elements[index]) {
|
7676
|
+
return this.elements[index].innerHTML.trim();
|
7677
|
+
}
|
7678
|
+
return null;
|
7679
|
+
},
|
7680
|
+
|
7681
|
+
checkContentChanged: function (editable) {
|
7682
|
+
editable = editable || MediumEditor.selection.getSelectionElement(this.options.contentWindow);
|
7683
|
+
this.events.updateInput(editable, { target: editable, currentTarget: editable });
|
7684
|
+
},
|
7685
|
+
|
7686
|
+
resetContent: function (element) {
|
7687
|
+
// For all elements that exist in the this.elements array, we can assume:
|
7688
|
+
// - Its initial content has been set in the initialContent object
|
7689
|
+
// - It has a medium-editor-index attribute which is the key value in the initialContent object
|
7690
|
+
|
7691
|
+
if (element) {
|
7692
|
+
var index = this.elements.indexOf(element);
|
7693
|
+
if (index !== -1) {
|
7694
|
+
this.setContent(initialContent[element.getAttribute('medium-editor-index')], index);
|
7695
|
+
}
|
7696
|
+
return;
|
7697
|
+
}
|
7698
|
+
|
7699
|
+
this.elements.forEach(function (el, idx) {
|
7700
|
+
this.setContent(initialContent[el.getAttribute('medium-editor-index')], idx);
|
7701
|
+
}, this);
|
7702
|
+
},
|
7703
|
+
|
7704
|
+
addElements: function (selector) {
|
7705
|
+
// Convert elements into an array
|
7706
|
+
var elements = createElementsArray(selector, this.options.ownerDocument, true);
|
7707
|
+
|
7708
|
+
// Do we have elements to add now?
|
7709
|
+
if (elements.length === 0) {
|
7710
|
+
return false;
|
7114
7711
|
}
|
7712
|
+
|
7713
|
+
elements.forEach(function (element) {
|
7714
|
+
// Initialize all new elements (we check that in those functions don't worry)
|
7715
|
+
element = initElement.call(this, element, this.id);
|
7716
|
+
|
7717
|
+
// Add new elements to our internal elements array
|
7718
|
+
this.elements.push(element);
|
7719
|
+
|
7720
|
+
// Trigger event so extensions can know when an element has been added
|
7721
|
+
this.trigger('addElement', { target: element, currentTarget: element }, element);
|
7722
|
+
}, this);
|
7723
|
+
},
|
7724
|
+
|
7725
|
+
removeElements: function (selector) {
|
7726
|
+
// Convert elements into an array
|
7727
|
+
var elements = createElementsArray(selector, this.options.ownerDocument),
|
7728
|
+
toRemove = elements.map(function (el) {
|
7729
|
+
// For textareas, make sure we're looking at the editor div and not the textarea itself
|
7730
|
+
if (el.getAttribute('medium-editor-textarea-id') && el.parentNode) {
|
7731
|
+
return el.parentNode.querySelector('div[medium-editor-textarea-id="' + el.getAttribute('medium-editor-textarea-id') + '"]');
|
7732
|
+
} else {
|
7733
|
+
return el;
|
7734
|
+
}
|
7735
|
+
});
|
7736
|
+
|
7737
|
+
this.elements = this.elements.filter(function (element) {
|
7738
|
+
// If this is an element we want to remove
|
7739
|
+
if (toRemove.indexOf(element) !== -1) {
|
7740
|
+
this.events.cleanupElement(element);
|
7741
|
+
if (element.getAttribute('medium-editor-textarea-id')) {
|
7742
|
+
cleanupTextareaElement(element);
|
7743
|
+
}
|
7744
|
+
// Trigger event so extensions can clean-up elements that are being removed
|
7745
|
+
this.trigger('removeElement', { target: element, currentTarget: element }, element);
|
7746
|
+
return false;
|
7747
|
+
}
|
7748
|
+
return true;
|
7749
|
+
}, this);
|
7750
|
+
}
|
7751
|
+
};
|
7752
|
+
|
7753
|
+
MediumEditor.getEditorFromElement = function (element) {
|
7754
|
+
var index = element.getAttribute('data-medium-editor-editor-index'),
|
7755
|
+
win = element && element.ownerDocument && (element.ownerDocument.defaultView || element.ownerDocument.parentWindow);
|
7756
|
+
if (win && win._mediumEditors && win._mediumEditors[index]) {
|
7757
|
+
return win._mediumEditors[index];
|
7115
7758
|
}
|
7759
|
+
return null;
|
7116
7760
|
};
|
7117
7761
|
}());
|
7118
7762
|
|
@@ -7154,7 +7798,7 @@ MediumEditor.parseVersionString = function (release) {
|
|
7154
7798
|
|
7155
7799
|
MediumEditor.version = MediumEditor.parseVersionString.call(this, ({
|
7156
7800
|
// grunt-bump looks for this:
|
7157
|
-
'version': '5.
|
7801
|
+
'version': '5.22.0'
|
7158
7802
|
}).version);
|
7159
7803
|
|
7160
7804
|
return MediumEditor;
|