PageSourceSearch

https://bsanimal.eu/vendor/wysiwyg/js/wysiwyg.js

js bsanimal.eu collected 2026-09-30 04:43:45 UTC 73,372 bytes, 3,451 lines download raw bytes

1/*! wysiwyg.js | https://github.com/wysiwygjs/wysiwyg.js | Apache License (v2) */
2
3(function(window, document, undefined)
4
5{
6
7    'use strict';
8
9
10
11    // http://stackoverflow.com/questions/97962/debounce-clicks-when-submitting-a-web-form
12
13    var debounce = function( callback, wait, cancelprevious )
14
15    {
16
17        var timeout;
18
19        return function()
20
21        {
22
23            if( timeout )
24
25            {
26
27                if( ! cancelprevious )
28
29                    return;
30
31                clearTimeout( timeout );
32
33            }
34
35            var context = this,
36
37                args = arguments;
38
39            timeout = setTimeout(
40
41                function()
42
43                {
44
45                    timeout = null;
46
47                    callback.apply( context, args );
48
49                }, wait );
50
51        };
52
53    };
54
55
56
57    // http://stackoverflow.com/questions/12949590/how-to-detach-event-in-ie-6-7-8-9-using-javascript
58
59    var addEvent = function( element, type, handler, useCapture )
60
61    {
62
63        element.addEventListener( type, handler, useCapture ? true : false );
64
65    };
66
67    var removeEvent = function( element, type, handler, useCapture )
68
69    {
70
71        element.removeEventListener( type, handler, useCapture ? true : false );
72
73    };
74
75    // prevent default
76
77    var cancelEvent = function( e )
78
79    {
80
81        e.preventDefault();
82
83        e.stopPropagation();
84
85    };
86
87
88
89    // http://stackoverflow.com/questions/2234979/how-to-check-in-javascript-if-one-element-is-a-child-of-another
90
91    var isOrContainsNode = function( ancestor, descendant, within )
92
93    {
94
95        var node = within ? descendant.parentNode : descendant;
96
97        while( node )
98
99        {
100
101            if( node === ancestor )
102
103                return true;
104
105            node = node.parentNode;
106
107        }
108
109        return false;
110
111    };
112
113    var isMediaNode = function( node )
114
115    {
116
117        var name = node.nodeName;
118
119        return name == 'IMG' || name == 'PICTURE' || name == 'SVG' || name == 'VIDEO' || name == 'AUDIO' ||
120
121            name == 'IFRAME' || name == 'MAP' || name == 'OBJECT' || name == 'EMBED';
122
123    };
124
125
126
127    // save/restore selection
128
129    // http://stackoverflow.com/questions/13949059/persisting-the-changes-of-range-objects-after-selection-in-html/13950376#13950376
130
131    var saveSelection = function( containerNode )
132
133    {
134
135        var sel = window.getSelection();
136
137        if( sel.rangeCount > 0 )
138
139            return sel.getRangeAt(0);
140
141        return null;
142
143    };
144
145    var restoreSelection = function( containerNode, savedSel )
146
147    {
148
149        if( ! savedSel )
150
151            return;
152
153        var sel = window.getSelection();
154
155        sel.removeAllRanges();
156
157        sel.addRange(savedSel);
158
159    };
160
161
162
163    // http://stackoverflow.com/questions/12603397/calculate-width-height-of-the-selected-text-javascript
164
165    // http://stackoverflow.com/questions/6846230/coordinates-of-selected-text-in-browser-page
166
167    var getSelectionRect = function()
168
169    {
170
171        var sel = window.getSelection();
172
173        if( ! sel.rangeCount )
174
175            return false;
176
177        var range = sel.getRangeAt(0).cloneRange();
178
179        var rect = range.getBoundingClientRect();
180
181        // Safari 5.1 returns null, IE9 returns 0/0/0/0 if image selected
182
183        if( rect && rect.left && rect.top && rect.right && rect.bottom )
184
185            return {
186
187                // Modern browsers return floating-point numbers
188
189                left: parseInt(rect.left),
190
191                top: parseInt(rect.top),
192
193                width: parseInt(rect.right - rect.left),
194
195                height: parseInt(rect.bottom - rect.top)
196
197            };
198
199        // on Webkit 'range.getBoundingClientRect()' sometimes return 0/0/0/0 - but 'range.getClientRects()' works
200
201        var rects = range.getClientRects ? range.getClientRects() : [];
202
203        for( var i=0; i < rects.length; ++i )
204
205        {
206
207            var rect = rects[i];
208
209            if( rect.left && rect.top && rect.right && rect.bottom )
210
211                return {
212
213                    // Modern browsers return floating-point numbers
214
215                    left: parseInt(rect.left),
216
217                    top: parseInt(rect.top),
218
219                    width: parseInt(rect.right - rect.left),
220
221                    height: parseInt(rect.bottom - rect.top)
222
223                };
224
225        }
226
227        return false;
228
229    };
230
231
232
233    var getSelectionCollapsed = function( containerNode )
234
235    {
236
237        var sel = window.getSelection();
238
239        if( sel.isCollapsed )
240
241            return true;
242
243        return false;
244
245    };
246
247
248
249    // http://stackoverflow.com/questions/7781963/js-get-array-of-all-selected-nodes-in-contenteditable-div
250
251    var getSelectedNodes = function( containerNode )
252
253    {
254
255        var sel = window.getSelection();
256
257        if( ! sel.rangeCount )
258
259            return [];
260
261        var nodes = [];
262
263        for( var i=0; i < sel.rangeCount; ++i )
264
265        {
266
267            var range = sel.getRangeAt(i),
268
269                node = range.startContainer,
270
271                endNode = range.endContainer;
272
273            while( node )
274
275            {
276
277                // add this node?
278
279                if( node != containerNode )
280
281                {
282
283                    var node_inside_selection = false;
284
285                    if( sel.containsNode )
286
287                        node_inside_selection = sel.containsNode( node, true );
288
289                    else // IE11
290
291                    {
292
293                        // http://stackoverflow.com/questions/5884210/how-to-find-if-a-htmlelement-is-enclosed-in-selected-text
294
295                        var noderange = document.createRange();
296
297                        noderange.selectNodeContents( node );
298
299                        for( var i=0; i < sel.rangeCount; ++i )
300
301                        {
302
303                            var range = sel.getRangeAt(i);
304
305                            // start after or end before -> skip node
306
307                            if( range.compareBoundaryPoints(range.END_TO_START,noderange) >= 0 &&
308
309                                range.compareBoundaryPoints(range.START_TO_END,noderange) <= 0 )
310
311                            {
312
313                                node_inside_selection = true;
314
315                                break;
316
317                            }
318
319                        }
320
321                    }
322
323                    if( node_inside_selection )
324
325                        nodes.push( node );
326
327                }
328
329                // http://stackoverflow.com/questions/667951/how-to-get-nodes-lying-inside-a-range-with-javascript
330
331                var nextNode = function( node, container )
332
333                {
334
335                    if( node.firstChild )
336
337                        return node.firstChild;
338
339                    while( node )
340
341                    {
342
343                        if( node == container ) // do not walk out of the container
344
345                            return null;
346
347                        if( node.nextSibling )
348
349                            return node.nextSibling;
350
351                        node = node.parentNode;
352
353                    }
354
355                    return null;
356
357                };
358
359                node = nextNode( node, node == endNode ? endNode : containerNode );
360
361            }
362
363        }
364
365        // Fallback
366
367        if( nodes.length == 0 && isOrContainsNode(containerNode,sel.focusNode) && sel.focusNode != containerNode )
368
369            nodes.push( sel.focusNode );
370
371        return nodes;
372
373    };
374
375
376
377    // http://stackoverflow.com/questions/8513368/collapse-selection-to-start-of-selection-not-div
378
379    var collapseSelectionEnd = function()
380
381    {
382
383        var sel = window.getSelection();
384
385        if( ! sel.isCollapsed )
386
387        {
388
389            // Form-submits via Enter throw 'NS_ERROR_FAILURE' on Firefox 34
390
391            try {
392
393                sel.collapseToEnd();
394
395            }
396
397            catch( e ) {
398
399            }
400
401        }
402
403    };
404
405
406
407    // http://stackoverflow.com/questions/15157435/get-last-character-before-caret-position-in-javascript
408
409    // http://stackoverflow.com/questions/11247737/how-can-i-get-the-word-that-the-caret-is-upon-inside-a-contenteditable-div
410
411    var expandSelectionCaret = function( containerNode, preceding, following )
412
413    {
414
415        var sel = window.getSelection();
416
417        if( sel.modify )
418
419        {
420
421            for( var i=0; i < preceding; ++i )
422
423                sel.modify('extend', 'backward', 'character');
424
425            for( var i=0; i < following;
425 ++i )
426
427                sel.modify('extend', 'forward', 'character');
428
429        }
430
431        else
432
433        {
434
435            // not so easy if the steps would cover multiple nodes ...
436
437            var range = sel.getRangeAt(0);
438
439            range.setStart( range.startContainer, range.startOffset - preceding );
440
441            range.setEnd( range.endContainer, range.endOffset + following );
442
443            sel.removeAllRanges();
444
445            sel.addRange(range);
446
447        }
448
449    };
450
451
452
453    // http://stackoverflow.com/questions/4652734/return-html-from-a-user-selected-text/4652824#4652824
454
455    var getSelectionHtml = function( containerNode )
456
457    {
458
459        if( getSelectionCollapsed( containerNode ) )
460
461            return null;
462
463        var sel = window.getSelection();
464
465        if( sel.rangeCount )
466
467        {
468
469            var container = document.createElement('div'),
470
471                len = sel.rangeCount;
472
473            for( var i=0; i < len; ++i )
474
475            {
476
477                var contents = sel.getRangeAt(i).cloneContents();
478
479                container.appendChild(contents);
480
481            }
482
483            return container.innerHTML;
484
485        }
486
487        return null;
488
489    };
490
491
492
493    var selectionInside = function( containerNode, force )
494
495    {
496
497        // selection inside editor?
498
499        var sel = window.getSelection();
500
501        if( isOrContainsNode(containerNode,sel.anchorNode) && isOrContainsNode(containerNode,sel.focusNode) )
502
503            return true;
504
505        // selection at least partly outside editor
506
507        if( ! force )
508
509            return false;
510
511        // force selection to editor
512
513        var range = document.createRange();
514
515        range.selectNodeContents( containerNode );
516
517        range.collapse( false );
518
519        sel.removeAllRanges();
520
521        sel.addRange(range);
522
523        return true;
524
525    };
526
527
528
529
530
531    // http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div/6691294#6691294
532
533    // http://stackoverflow.com/questions/4823691/insert-an-html-element-in-a-contenteditable-element
534
535    // http://stackoverflow.com/questions/6139107/programatically-select-text-in-a-contenteditable-html-element
536
537    var pasteHtmlAtCaret = function( containerNode, html )
538
539    {
540
541        var sel = window.getSelection();
542
543        if( sel.getRangeAt && sel.rangeCount )
544
545        {
546
547            var range = sel.getRangeAt(0);
548
549            // Range.createContextualFragment() would be useful here but is
550
551            // only relatively recently standardized and is not supported in
552
553            // some browsers (IE9, for one)
554
555            var el = document.createElement('div');
556
557            el.innerHTML = html;
558
559            var frag = document.createDocumentFragment(), node, lastNode;
560
561            while ( (node = el.firstChild) ) {
562
563                lastNode = frag.appendChild(node);
564
565            }
566
567            if( isOrContainsNode(containerNode, range.commonAncestorContainer) )
568
569            {
570
571                range.deleteContents();
572
573                range.insertNode(frag);
574
575            }
576
577            else {
578
579                containerNode.appendChild(frag);
580
581            }
582
583            // Preserve the selection
584
585            if( lastNode )
586
587            {
588
589                range = range.cloneRange();
590
591                range.setStartAfter(lastNode);
592
593                range.collapse(true);
594
595                sel.removeAllRanges();
596
597                sel.addRange(range);
598
599            }
600
601        }
602
603    };
604
605
606
607    // Create editor
608
609    var wysiwyg = function( element, options )
610
611    {
612
613        var toolbar = options.toolbar,
614
615            buttons = options.buttons, selectionbuttons = options.selectionbuttons,
616
617            suggester = options.suggester, interceptenter = options.interceptenter, hijackContextmenu = !!options.hijackmenu,
618
619            node_container = typeof(element) == 'string' ? document.querySelector(element) : element,
620
621            node_textarea = node_container.querySelector('textarea'),
622
623            commands, hotkeys = {},
624
625            toolbar_top = toolbar == 'top', toolbar_bottom = toolbar == 'bottom', toolbar_demand = toolbar == 'demand';
626
627
628
629        // initialize editor
630
631        var add_class = function( element, classname )
632
633        {
634
635            if( element.classList )
636
637                element.classList.add( classname );
638
639            else // IE9
640
641                element.className += ' ' + classname;
642
643        };
644
645        var remove_class = function( element, classname )
646
647        {
648
649            if( element.classList )
650
651                element.classList.remove( classname );
652
653        }
654
655        var node_contenteditable = node_container.querySelector('[contenteditable=true]');
656
657        if( ! node_contenteditable )
658
659        {
660
661            node_contenteditable = document.createElement( 'div' );
662
663            node_contenteditable.setAttribute( 'contentEditable', 'true' );
664
665            var placeholder = node_textarea.placeholder;
666
667            if( placeholder )
668
669                node_contenteditable.setAttribute( 'data-placeholder', placeholder );
670
671            node_container.insertBefore( node_contenteditable, node_container.firstChild );
672
673        }
674
675
676
677        // Simulate ':focus-within'
678
679        var remove_focus_timeout = null;
680
681        var add_class_focus = function()
682
683        {
684
685            if( remove_focus_timeout )
686
687                clearTimeout( remove_focus_timeout );
688
689            remove_focus_timeout = null;
690
691            add_class( node_container, 'focus' );
692
693            if( toolbar_demand )
694
695                add_class( node_container, 'focused' );
696
697        };
698
699        var remove_class_focus = function()
700
701        {
702
703            if( remove_focus_timeout || document.activeElement == node_contenteditable )
704
705                return ;
706
707            remove_focus_timeout = setTimeout( function() {
708
709                remove_focus_timeout = null;
710
711                remove_class( node_container, 'focus' );
712
713            }, 50 );
714
715        };
716
717        addEvent( node_contenteditable, 'focus', add_class_focus );
718
719        addEvent( node_contenteditable, 'blur', remove_class_focus );
720
721        // register form-reset
722
723        if( node_textarea && node_textarea.form )
724
725            addEvent( node_textarea.form, 'reset', remove_class_focus );
726
727
728
729        // Insert-link popup
730
731        var create_insertlink = function( popup, modify_a_href )
732
733        {
734
735            var textbox = document.createElement('input');
736
737            textbox.placeholder = 'www.example.com';
738
739            if( modify_a_href )
740
741                textbox.value = modify_a_href.href;
742
743            textbox.style.width = '20em';
744
745            textbox.style.maxWidth = parseInt(node_container.offsetWidth *2/3) + 'px';
746
747            textbox.autofocus = true;
748
749            if( modify_a_href )
750
751                addEvent( textbox, 'input', function( e )
752
753                {
754
755                    var url = textbox.value.trim();
756
757                    if( url )
758
759                        modify_a_href.href = url;
760
761                });
762
763            addEvent( textbox, 'keypress', function( e )
764
765            {
766
767                var key = e.which || e.keyCode;
768
769                if( key != 13 )
770
771                    return;
772
773                var url = textbox.value.trim();
774
775                if( modify_a_href )
776
777                    ;
778
779                else if( url )
780
781                {
782
783                    var url_scheme = url;
784
785                    if( ! /^[a-z0-9]+:\/\//.test(url) )
786
787                        url_scheme = "http://" + url;
788
789                    if( commands.getSelectedHTML() )
790
791                        commands.insertLink( url_scheme );
792
793                    else
794
795                    {
796
797                        // Encode html entities - http://stackoverflow.com/questions/5499078/fastest-method-to-escape-html-tags-as-html-entities
798
799                        var htmlencode = function( text ) {
800
801                            return text.replace(/[&<>"]/g, function(tag)
802
803                            {
804
805                                var charsToReplace = { '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;' };
806
807                                return charsToReplace[tag] || tag;
808
809                            });
810
811                        }
812
813                        commands.insertHTML( '<a href="' + htmlencode(url_scheme) + '">' + htmlencode(url) + '</a>' );
814
815                    }
816
817                }
818
819                commands.closePopup().collapseSelection();
820
821                node_contenteditable.focus();
822
823            });
824
825            popup.appendChild( textbox );
826
827            // set focus
828
829            window.setTimeout( function()
830
831            {
832
833                textbox.focus();
834
835                add_class_focus();
836
837            }, 1 );
838
839        };
840
841
842
843        // Color-palette popup
844
845        var create_colorpalette = function( popup, forecolor )
846
847        {
848
849            // http://stackoverflow.com/questions/17242144/javascript-convert-hsb-hsv-color-to-rgb-accurately
850
851            var HSVtoRGB = function( h, s, v )
852
853            {
854
855                var r, g, b, i, f, p, q, t;
856
857                i = Math.floor(h * 6);
858
859                f = h * 6 - i;
860
861                p = v * (1 - s);
862
863                q = v * (1 - f * s);
864
865                t = v * (1 - (1 - f) * s);
866
867                switch (i % 6)
868
869                {
870
871                    case 0: r = v, g = t, b = p; break;
872
873                    case 1: r = q, g = v, b = p; break;
874
875                    case 2: r = p, g = v, b = t; break;
876
877                    case 3: r = p, g = q, b = v; break;
878
879                    case 4: r = t, g = p, b = v; break;
880
881                    case 5: r = v, g = p, b = q; break;
882
883                }
884
885                var hr = Math.floor(r * 255).toString(16);
886
887                var hg = Math.floor(g * 255).toString(16);
888
889                var hb = Math.floor(b * 255).toString(16);
890
891                return '#' + (hr.length < 2 ? '0' : '') + hr +
892
893                    (hg.length < 2 ? '0' : '') + hg +
894
895                    (hb.length < 2 ? '0' : '') + hb;
896
897            };
898
899            // create table
900
901            var table = document.createElement('table');
902
903            table.style.borderCollapse = 'collapse';
904
905            for( var row=1; row < 15; ++row ) // should be '16' - but last line looks so dark
906
907            {
908
909                var tr = document.createElement('tr');
910
911                for( var col=0; col < 25; ++col ) // last column is grayscale
912
913                {
914
915                    var color;
916
917                    if( col == 24 )
918
919                    {
920
921                        var gray = Math.floor(255 / 13 * (14 - row)).toString(16);
922
923                        var hexg = (gray.length < 2 ? '0' : '') + gray;
924
925                        color = '#' + hexg + hexg + hexg;
926
927                    }
928
929                    else
930
931                    {
932
933                        var hue        = col / 24;
934
935                        var saturation = row <= 8 ? row     /8 : 1;
936
937                        var value      = row  > 8 ? (16-row)/8 : 1;
938
939                        color = HSVtoRGB( hue, saturation, value );
940
941                    }
942
943                    var td = document.createElement('td');
944
945                    td.style.width = '0.5em';
946
947                    td.style.height = '0.5em';
948
949                    td.style.cursor = 'pointer';
950
951                    td.style.backgroundColor = color;
952
953                    td.title = color;
954
955                    td.style.userSelect = 'none';
956
957                    addEvent( td, 'click', function( e )
958
959                    {
960
961                        var color = this.title;
962
963                        if( forecolor )
964
965                            commands.forecolor( color ).closePopup().collapseSelection();
966
967                        else
968
969                            commands.highlight( color ).closePopup().collapseSelection();
970
971                        cancelEvent( e );
972
973                    });
974
975                    tr.appendChild( td );
976
977                }
978
979                table.appendChild( tr );
980
981            }
982
983            popup.appendChild( table );
984
985        };
986
987
988
989        // read file as data-url
990
991        var filecontents = function( file, callback )
992
993        {
994
995            // base64 a 2GB video is insane: artificial clamp at 64MB
996
997            if( file.size > 0x4000000 )
998
999            {
1000
1001                callback();
1002
1003                return;
1004
1005            }
1006
1007
1008
1009            // read file as data-url
1010
1011            var normalize_dataurl = function( orientation )
1012
1013            {
1014
1015                var filereader = new FileReader();
1016
1017                filereader.onload = function( e )
1018
1019                {
1020
1021                    if( ! orientation || orientation == 1 || orientation > 8 )
1022
1023                        return callback( file.type, e.target.result );
1024
1025                    // normalize
1026
1027                    var img = new Image();
1028
1029                    img.src = e.target.result;
1030
1031                    img.onload = function()
1032
1033                    {
1034
1035                        var width = img.width;
1036
1037                        var height = img.height;
1038
1039                        if( width > height )
1040
1041                        {
1042
1043                            var max_width = 4096;
1044
1045                            if( width > max_width )
1046
1047                            {
1048
1049                                height *= max_width / width;
1050
1051                                width = max_width;
1052
1053                            }
1054
1055                        }
1056
1057                        else
1058
1059                        {
1060
1061                            var max_height = 4096;
1062
1063                            if( height > max_height )
1064
1065                            {
1066
1067                                width *= max_height / height;
1068
1069                                height = max_height;
1070
1071                            }
1072
1073                        }
1074
1075                        var canvas = document.createElement( 'canvas' );
1076
1077                        var ctx = canvas.getContext( '2d' );
1078
1079                        canvas.width = width;
1080
1081                        canvas.height = height;
1082
1083                        ctx.save();
1084
1085                        if( orientation > 4 )
1086
1087                        {
1088
1089                            canvas.width = height;
1090
1091                            canvas.height = width;
1092
1093                        }
1094
1095                        switch( orientation )
1096
1097                        {
1098
1099                            case 2: ctx.translate( width, 0 );
1100
1101                                ctx.scale( -1, 1 );
1102
1103                                break;
1104
1105                            case 3: ctx.translate( width, height );
1106
1107                                ctx.rotate( Math.PI );
1108
1109                                break;
1110
1111                            case 4: ctx.translate( 0, height );
1112
1113                                ctx.scale( 1, -1 );
1114
1115                                break;
1116
1117                            case 5: ctx.rotate( 0.5 * Math.PI );
1118
1119                                ctx.scale( 1, -1 );
1120
1121                                break;
1122
1123                            case 6: ctx.rotate( 0.5 * Math.PI );
1124
1125                                ctx.translate( 0, -height );
1126
1127                                break;
1128
1129                            case 7: ctx.rotate( 0.5 * Math.PI );
1130
1131                                ctx.translate( width, -height );
1132
1133                                ctx.scale( -1, 1 );
1134
1135                                break;
1136
1137                            case 8: ctx.rotate( -0.5 * Math.PI );
1138
1139                                ctx.translate( -width, 0 );
1140
1141                                break;
1142
1143                        }
1144
1145                        ctx.drawImage( img, 0, 0, width, height );
1146
1147                        ctx.restore();
1148
1149                        var dataURL = canvas.toDataURL( 'image/jpeg', 0.99 );
1150
1151                        callback( file.type, dataURL );
1152
1153                    }
1154
1155                };
1156
1157                filereader.onerror = function( e )
1158
1159                {
1160
1161                    callback();
1162
1163                };
1164
1165                filereader.readAsDataURL( file );
1166
1167            }
1168
1169            if( ! window.DataView )
1170
1171                return normalize_dataurl();
1172
1173
1174
1175            // get orientation - https://stackoverflow.com/questions/7584794/accessing-jpeg-exif-rotation-data-in-javascript-on-the-client-side
1176
1177            var filereader = new FileReader();
1178
1179            filereader.onload = function( e )
1180
1181            {
1182
1183                var contents = e.target.result;
1184
1185                var view = new DataView( contents );
1186
1187                // Not a JPEG at all?
1188
1189                if( view.getUint16(0, false) != 0xFFD8 )
1190
1191                    return normalize_dataurl();
1192
1193                var length = view.byteLength,
1194
1195                    offset = 2;
1196
1197                while( offset < length )
1198
1199                {
1200
1201                    // Missing EXIF?
1202
1203                    if( view.getUint16(offset+2, false) <= 8 )
1204
1205                        return normalize_dataurl();
1206
1207                    var marker = view.getUint16(offset, false);
1208
1209                    offset += 2;
1210
1211                    if( marker == 0xFFE1 )
1212
1213                    {
1214
1215                        if( view.getUint32(offset += 2, false) != 0x45786966 )
1216
1217                            return normalize_dataurl();
1218
1219                        var little = view.getUint16( offset += 6, false ) == 0x4949;
1220
1221                        offset += view.getUint32( offset + 4, little );
1222
1223                        var tags = view.getUint16( offset, little );
1224
1225                        offset += 2;
1226
1227                        for( var i=0; i < tags; ++i )
1228
1229                        {
1230
1231                            if( view.getUint16(offset + (i * 12), little) == 0x0112 )
1232
1233                            {
1234
1235                                var orientation = view.getUint16( offset + (i * 12) + 8, little );
1236
1237                                return normalize_dataurl( orientation );
1238
1239                            }
1240
1241                        }
1242
1243                    }
1244
1245                    else if( (marker & 0xFF00) != 0xFF00 )
1246
1247                        break;
1248
1249                    else
1250
1251                        offset += view.getUint16( offset, false );
1252
1253                }
1254
1255                return normalize_dataurl();
1256
1257            };
1258
1259            filereader.onerror = function( e )
1260
1261            {
1262
1263                callback();
1264
1265            };
1266
1267            filereader.readAsArrayBuffer( file );
1268
1269        };
1270
1271
1272
1273        // open popup and apply position
1274
1275        var popup_position = function( popup, left, top ) // left+top relative to container
1276
1277        {
1278
1279            // Test parents, el.getBoundingClientRect() does not work within 'position:fixed'
1280
1281            var node = node_container,
1282
1283                popup_parent = node.offsetParent;
1284
1285            while( node )
1286
1287            {
1288
1289                var node_style = getComputedStyle( node );
1290
1291                if( node_style['position'] != 'static' )
1292
1293                    break;
1294
1295                left += node.offsetLeft;
1296
1297                top += node.offsetTop;
1298
1299                popup_parent = node;
1300
1301                node = node.offsetParent;
1302
1303            }
1304
1305            // Move popup as high as possible in the DOM tree
1306
1307            popup_parent.appendChild( popup );
1308
1309            // Trim to viewport
1310
1311            var rect = popup_parent.getBoundingClientRect();
1312
1313            var documentElement = document.documentElement;
1314
1315            var viewport_width = Math.min( window.innerWidth, Math.max(documentElement.offsetWidth, documentElement.scrollWidth) );
1316
1317            var viewport_height = window.innerHeight;
1318
1319            var popup_width = popup.offsetWidth;    // accurate to integer
1320
1321            var popup_height = popup.offsetHeight;
1322
1323            if( rect.left + left < 1 )
1324
1325                left = 1 - rect.left;
1326
1327            else if( rect.left + left + popup_width > viewport_width - 1 )
1328
1329                left = Math.max( 1 - rect.left, viewport_width - 1 - rect.left - popup_width );
1330
1331            if( rect.top + top < 1 )
1332
1333                top = 1 - rect.top;
1334
1335            else if( rect.top + top + popup_height > viewport_height - 1 )
1336
1337                top = Math.max( 1 - rect.top, viewport_height - 1 - rect.top - popup_height );
1338
1339            // Set offset
1340
1341            popup.style.left = parseInt(left) + 'px';
1342
1343            popup.style.top = parseInt(top) + 'px';
1344
1345        };
1346
1347        // open popup and apply position
1348
1349        var popup_type = null;
1350
1351        var create_popup = function( down, type, create_content, argument )
1352
1353        {
1354
1355            // popup already open?
1356
1357            var popup = commands.activePopup();
1358
1359            if( popup && popup_type === type )
1360
1361            {
1362
1363                remove_class( popup, 'animate-down' );
1364
1365                remove_class( popup, 'animate-up' );
1366
1367            }
1368
1369            else
1370
1371            {
1372
1373                // either run 'commands.closePopup().openPopup()' or remove children
1374
1375                popup = commands.openPopup();
1376
1377                add_class( popup, 'wysiwyg-popup' );
1378
1379                add_class( popup, down ? 'animate-down' : 'animate-up' );
1380
1381                popup_type = type;
1382
1383            }
1384
1385            // re-fill content
1386
1387            while( popup.firstChild )
1388
1389                popup.removeChild( popup.firstChild );
1390
1391            create_content( popup, argument );
1392
1393            return popup;
1394
1395        };
1396
1397        var open_popup_button = function( button, type, create_content, argument )
1398
1399        {
1400
1401            var popup = create_popup( toolbar_top ? true : false, t
1401ype, create_content, argument );
1402
1403            // Popup position - point to top/bottom-center of the button
1404
1405            var container_offset = node_container.getBoundingClientRect();
1406
1407            var button_offset = button.getBoundingClientRect();
1408
1409            var left = button_offset.left - container_offset.left +
1410
1411                parseInt(button.offsetWidth / 2) - parseInt(popup.offsetWidth / 2);
1412
1413            var top = button_offset.top - container_offset.top;
1414
1415            if( toolbar_top )
1416
1417                top += button.offsetHeight;
1418
1419            else
1420
1421                top -= popup.offsetHeight;
1422
1423            popup_position( popup, left, top );
1424
1425        };
1426
1427        var popup_selection_position = function( popup, rect )
1428
1429        {
1430
1431            // Popup position - point to center of the selection
1432
1433            var container_offset = node_container.getBoundingClientRect();
1434
1435            var contenteditable_offset = node_contenteditable.getBoundingClientRect();
1436
1437            var left = rect.left + parseInt(rect.width / 2) - parseInt(popup.offsetWidth / 2) + contenteditable_offset.left - container_offset.left;
1438
1439            var top = rect.top + rect.height + contenteditable_offset.top - container_offset.top;
1440
1441            popup_position( popup, left, top );
1442
1443        };
1444
1445        var open_popup_selection = function( rect, type, create_content, argument )
1446
1447        {
1448
1449            var popup = create_popup( true, type, create_content, argument );
1450
1451            popup_selection_position( popup, rect );
1452
1453        };
1454
1455
1456
1457        // Fill buttons (on toolbar or on selection)
1458
1459        var recent_selection_rect = null, recent_selection_link = null
1460
1461        var fill_buttons = function( toolbar_container, selection_rect, buttons, hotkeys )
1462
1463        {
1464
1465            buttons.forEach( function(button)
1466
1467            {
1468
1469                // Custom button
1470
1471                if( button instanceof HTMLElement )
1472
1473                {
1474
1475                    toolbar_container.appendChild( button );
1476
1477                    // Simulate ':focus-within'
1478
1479                    addEvent( button, 'focus', add_class_focus );
1480
1481                    addEvent( button, 'blur', remove_class_focus );
1482
1483                    return;
1484
1485                }
1486
1487
1488
1489                // Create a button
1490
1491                var element = document.createElement( 'button' );
1492
1493                add_class( element, 'btn' );
1494
1495                if( 'icon' in button )
1496
1497                {
1498
1499                    var htmlparser = document.implementation.createHTMLDocument('');
1500
1501                    htmlparser.body.innerHTML = button.icon;
1502
1503                    for( var child=htmlparser.body.firstChild; child !== null; child=child.nextSibling )
1504
1505                        element.appendChild( child );
1506
1507                }
1508
1509                if( 'attr' in button )
1510
1511                {
1512
1513                    for( var key in button.attr )
1514
1515                        element.setAttribute( key, button.attr[key] );
1516
1517                }
1518
1519                // Simulate ':focus-within'
1520
1521                addEvent( element, 'focus', add_class_focus );
1522
1523                addEvent( element, 'blur', remove_class_focus );
1524
1525
1526
1527                // Create handler
1528
1529                var handler = null;
1530
1531                if( 'click' in button )
1532
1533                    handler = function()
1534
1535                    {
1536
1537                        button.click( commands, element );
1538
1539                    };
1540
1541                else if( 'popup' in button )
1542
1543                    handler = function()
1544
1545                    {
1546
1547                        var fill_popup = function( popup )
1548
1549                        {
1550
1551                            button.popup( commands, popup, element );
1552
1553                        };
1554
1555                        if( selection_rect )
1556
1557                            open_popup_selection( selection_rect, fill_popup.toString(), fill_popup );
1558
1559                        else
1560
1561                            open_popup_button( element, fill_popup.toString(), fill_popup );
1562
1563                    };
1564
1565                else if( 'browse' in button || 'dataurl' in button )
1566
1567                    handler = function()
1568
1569                    {
1570
1571                        // remove popup
1572
1573                        commands.closePopup().collapseSelection();
1574
1575                        // open browse dialog
1576
1577                        var input = document.createElement( 'input' );
1578
1579                        input.type = 'file';
1580
1581                        input.multiple = true;
1582
1583                        input.style.display = 'none';
1584
1585                        addEvent( input, 'change', function( e )
1586
1587                        {
1588
1589                            var remove_input = 'dataurl' in button;
1590
1591                            if( ! e.target.files )
1592
1593                                remove_input = true;
1594
1595                            else
1596
1597                            {
1598
1599                                var files = evt.target.files;
1600
1601                                for( var i=0; i < files.length; ++i )   // can't use forEach() with 'FileList'
1602
1603                                {
1604
1605                                    var file = files[i];
1606
1607                                    if( 'browse' in button )
1608
1609                                        button.browse( commands, input,file, element );
1610
1611                                    else
1612
1613                                    {
1614
1615                                        // Keep callback-order - supporting browser without 'Promise'
1616
1617                                        var callbacks = [],
1618
1619                                            callnext = 0;
1620
1621                                        (function(i)
1622
1623                                        {
1624
1625                                            filecontents( file,
1626
1627                                                function( type, dataurl )
1628
1629                                                {
1630
1631                                                    callbacks[i] = function() {
1632
1633                                                        if( dataurl )   // empty on error
1634
1635                                                            button.dataurl( commands, type, dataurl, element );
1636
1637                                                    };
1638
1639                                                    // trigger callbacks in order
1640
1641                                                    while( callnext in callbacks )
1642
1643                                                    {
1644
1645                                                        callbacks[callnext]();
1646
1647                                                        callnext++;
1648
1649                                                    }
1650
1651                                                    if( callnext == files.length )
1652
1653                                                        callbacks = null;
1654
1655                                                });
1656
1657                                        })(i);
1658
1659                                    }
1660
1661                                }
1662
1663                            }
1664
1665                            if( remove_input )
1666
1667                                input.parentNode.removeChild( input );
1668
1669                            cancelEvent( e );
1670
1671                        });
1672
1673                        node_container.appendChild( input );
1674
1675                        var evt = document.createEvent( 'MouseEvents' );
1676
1677                        evt.initEvent( 'click', true, false );
1678
1679                        input.dispatchEvent( evt );
1680
1681                    };
1682
1683                else if( 'action' in button )
1684
1685                    handler = function()
1686
1687                    {
1688
1689                        switch( button.action )
1690
1691                        {
1692
1693                            case 'link':
1694
1695                                if( selection_rect )
1696
1697                                    open_popup_selection( selection_rect, 'link', create_insertlink, recent_selection_link );
1698
1699                                else
1700
1701                                    open_popup_button( element, 'link', create_insertlink, recent_selection_link );
1702
1703                                break;
1704
1705                            case 'bold':
1706
1707                                commands.bold().closePopup().collapseSelection();
1708
1709                                break;
1710
1711                            case 'italic':
1712
1713                                commands.italic().closePopup().collapseSelection();
1714
1715                                break;
1716
1717                            case 'underline':
1718
1719                                commands.underline().closePopup().collapseSelection();
1720
1721                                break;
1722
1723                            case 'strikethrough':
1724
1725                                commands.strikethrough().closePopup().collapseSelection();
1726
1727                                break;
1728
1729                            case 'colortext':
1730
1731                                if( selection_rect )
1732
1733                                    open_popup_selection( selection_rect, 'colortext', create_colorpalette, true );
1734
1735                                else
1736
1737                                    open_popup_button( element, 'colortext', create_colorpalette, true );
1738
1739                                break;
1740
1741                            case 'colorfill':
1742
1743                                if( selection_rect )
1744
1745                                    open_popup_selection( selection_rect, 'colorfill', create_colorpalette, false );
1746
1747                                else
1748
1749                                    open_popup_button( element, 'colorfill', create_colorpalette, false );
1750
1751                                break;
1752
1753                            case 'subscript':
1754
1755                                commands.subscript().closePopup().collapseSelection();
1756
1757                                break;
1758
1759                            case 'superscript':
1760
1761                                commands.superscript().closePopup().collapseSelection();
1762
1763                                break;
1764
1765                            case 'orderedlist':
1766
1767                                commands.orderedList().closePopup().collapseSelection();
1768
1769                                break;
1770
1771                            case 'unorderedlist':
1772
1773                                commands.unorderedList().closePopup().collapseSelection();
1774
1775                                break;
1776
1777                            case 'clearformat':
1778
1779                                commands.removeFormat().closePopup().collapseSelection();
1780
1781                                break;
1782
1783                        }
1784
1785                    };
1786
1787                element.onclick = function( e )
1788
1789                {
1790
1791                    if( handler )
1792
1793                        handler();
1794
1795                    cancelEvent( e );
1796
1797                };
1798
1799                toolbar_container.appendChild( element );
1800
1801
1802
1803                // Hotkey
1804
1805                if( 'hotkey' in button && handler && hotkeys )
1806
1807                    hotkeys[button.hotkey.toLowerCase()] = handler;
1808
1809            });
1810
1811        };
1812
1813
1814
1815        // Handle suggester
1816
1817        var typed_suggestion = null, suggestion_sequence = 1, first_suggestion_html = null;
1818
1819        var finish_suggestion = function( insert_html )
1820
1821        {
1822
1823            // fire suggestion
1824
1825            if( insert_html )
1826
1827                commands.expandSelection( typed_suggestion.length, 0 ).insertHTML( insert_html );
1828
1829            typed_suggestion = null;
1830
1831            first_suggestion_html = null;
1832
1833            suggestion_sequence += 1;
1834
1835            commands.closePopup();
1836
1837        };
1838
1839        var suggester_keydown = function( key, character, shiftKey, altKey, ctrlKey, metaKey )
1840
1841        {
1842
1843            if( key == 13 && first_suggestion_html )
1844
1845            {
1846
1847                finish_suggestion( first_suggestion_html );
1848
1849                return false;   // swallow enter
1850
1851            }
1852
1853            return true;
1854
1855        };
1856
1857        var ask_suggestions = function()
1858
1859        {
1860
1861            if( ! typed_suggestion )
1862
1863                return;
1864
1865            var current_sequence = suggestion_sequence;
1866
1867            var open_suggester = function( suggestions )
1868
1869            {
1870
1871                if( ! recent_selection_rect || current_sequence != suggestion_sequence )
1872
1873                    return;
1874
1875                first_suggestion_html = null;
1876
1877                // Empty suggestions means: stop suggestion handling
1878
1879                if( ! suggestions )
1880
1881                {
1882
1883                    finish_suggestion();
1884
1885                    return;
1886
1887                }
1888
1889                // Show suggester
1890
1891                var fill_popup = function( popup )
1892
1893                {
1894
1895                    suggestions.forEach( function( suggestion )
1896
1897                    {
1898
1899                        var element = document.createElement('div');
1900
1901                        add_class( element, 'suggestion' );
1902
1903                        element.innerHTML = suggestion.label;
1904
1905                        addEvent( element, 'click', function( e )
1906
1907                        {
1908
1909                            finish_suggestion( suggestion.insert );
1910
1911                            cancelEvent( e );
1912
1913                        });
1914
1915                        popup.appendChild( element );
1916
1917
1918
1919                        // Store suggestion to handle 'Enter'
1920
1921                        if( first_suggestion_html === null )
1922
1923                            first_suggestion_html = suggestion.insert;
1924
1925                    });
1926
1927                };
1928
1929                open_popup_selection( recent_selection_rect, 'suggestion', fill_popup );
1930
1931            };
1932
1933            // Ask to start/continue a suggestion
1934
1935            if( ! suggester(typed_suggestion, open_suggester) )
1936
1937                finish_suggestion();
1938
1939        };
1940
1941        var debounced_suggestions = debounce( ask_suggestions, 100, true );
1942
1943        var suggester_keypress = function( key, character, shiftKey, altKey, ctrlKey, metaKey )
1944
1945        {
1946
1947            // Special keys
1948
1949            switch( key )
1950
1951            {
1952
1953                case  8: // backspace
1954
1955                    if( typed_suggestion )
1956
1957                        typed_suggestion = typed_suggestion.slice( 0, -1 );
1958
1959                    if( typed_suggestion )  // if still text -> continue, else abort
1960
1961                        break;
1962
1963                    finish_suggestion();
1964
1965                    return true;
1966
1967                case 13: // enter
1968
1969                case 27: // escape
1970
1971                case 33: // pageUp
1972
1973                case 34: // pageDown
1974
1975                case 35: // end
1976
1977                case 36: // home
1978
1979                case 37: // left
1980
1981                case 38: // up
1982
1983                case 39: // right
1984
1985                case 40: // down
1986
1987                    if( typed_suggestion )
1988
1989                        finish_suggestion();
1990
1991                    return true;
1992
1993                default:
1994
1995                    if( ! typed_suggestion )
1996
1997                        typed_suggestion = '';
1998
1999                    typed_suggestion += character;
2000
2001                    break;
2002
2003            }
2004
2005            // Throttle requests
2006
2007            debounced_suggestions();
2008
2009            return true;
2010
2011        };
2012
2013
2014
2015        // Create contenteditable
2016
2017        var onKeyDown = function( key, character, shiftKey, altKey, ctrlKey, metaKey )
2018
2019        {
2020
2021            // submit form on enter-key
2022
2023            if( interceptenter && key == 13 && ! shiftKey && ! altKey && ! ctrlKey && ! metaKey )
2024
2025            {
2026
2027                commands.sync();
2028
2029                if( interceptenter() )
2030
2031                {
2032
2033                    commands.closePopup();
2034
2035                    return false; // swallow enter
2036
2037                }
2038
2039            }
2040
2041            // Exec hotkey (onkeydown because e.g. CTRL+B would oben the bookmarks)
2042
2043            if( character && !shiftKey && !altKey && ctrlKey && !metaKey )
2044
2045            {
2046
2047                var hotkey = character.toLowerCase();
2048
2049                if( hotkeys[hotkey] )
2050
2051                {
2052
2053                    hotkeys[hotkey]();
2054
2055                    return false; // prevent default
2056
2057                }
2058
2059            }
2060
2061            // Handle suggester
2062
2063            if( suggester )
2064
2065                return suggester_keydown( key, character, shiftKey, altKey, ctrlKey, metaKey );
2066
2067        };
2068
2069        var onKeyPress = function( key, character, shiftKey, altKey, ctrlKey, metaKey )
2070
2071        {
2072
2073            // Handle suggester
2074
2075            if( suggester )
2076
2077                return suggester_keypress( key, character, shiftKey, altKey, ctrlKey, metaKey );
2078
2079        };
2080
2081        //var onKeyUp = function( key, character, shiftKey, altKey, ctrlKey, metaKey )
2082
2083        //{
2084
2085        //};
2086
2087        var onSelection = function( collapsed, rect, nodes, rightclick )
2088
2089        {
2090
2091            recent_selection_rect = collapsed ? rect || recent_selection_rect : null;
2092
2093            recent_selection_link = null;
2094
2095            // Fix type error - https://github.com/wysiwygjs/wysiwyg.js/issues/4
2096
2097            if( ! rect )
2098
2099            {
2100
2101                finish_suggestion();
2102
2103                return;
2104
2105            }
2106
2107            // Collapsed selection
2108
2109            if( collapsed )
2110
2111            {
2112
2113                // Active suggestion: apply toolbar-position
2114
2115                if( typed_suggestion !== null )
2116
2117                {
2118
2119                    var popup = commands.activePopup();
2120
2121                    if( popup )
2122
2123                    {
2124
2125                        remove_class( popup, 'animate-down' );
2126
2127                        remove_class( popup, 'animate-up' );
2128
2129                        popup_selection_position( popup, rect );
2130
2131                    }
2132
2133                    return;
2134
2135                }
2136
2137            }
2138
2139            // Click on a link opens the link-popup
2140
2141            for( var i=0; i < nodes.length; ++i )
2142
2143            {
2144
2145                var node = nodes[i];
2146
2147                var closest = node.closest ||   // latest
2148
2149                    function( selector ) {      // IE + Edge - https://github.com/nefe/You-Dont-Need-jQuery
2150
2151                        var node = this;
2152
2153                        while( node )
2154
2155                        {
2156
2157                            var matchesSelector = node.matches || node.webkitMatchesSelector || node.mozMatchesSelector || node.msMatchesSelector;
2158
2159                            if( matchesSelector && matchesSelector.call(node, selector) )
2160
2161                                return node;
2162
2163                            node = node.parentElement;
2164
2165                        }
2166
2167                        return null;
2168
2169                    };
2170
2171                recent_selection_link = closest.call( node, 'a' );
2172
2173                if( recent_selection_link )
2174
2175                    break;
2176
2177            }
2178
2179            // Show selection popup?
2180
2181            var show_popup = true;
2182
2183            // 'right-click' always opens the popup
2184
2185            if( rightclick )
2186
2187                ;
2188
2189            // No selection-popup wanted?
2190
2191            else if( ! selectionbuttons )
2192
2193                show_popup = false;
2194
2195            // Selected popup wanted, but nothing selected (=selection collapsed)
2196
2197            else if( collapsed )
2198
2199                show_popup = false;
2200
2201            // Image selected -> skip toolbar-popup (better would be an 'image-popup')
2202
2203            else nodes.forEach( function(node)
2204
2205                {
2206
2207                    if( isMediaNode(node) )
2208
2209                        show_popup = false;
2210
2211                });
2212
2213            if( ! show_popup )
2214
2215            {
2216
2217                finish_suggestion();
2218
2219                return;
2220
2221            }
2222
2223            // fill buttons
2224
2225            open_popup_selection( rect, 'selection', function( popup )
2226
2227            {
2228
2229                var toolbar_element = document.createElement('div');
2230
2231                add_class( toolbar_element, 'toolbar' );
2232
2233                popup.appendChild( toolbar_element );
2234
2235                fill_buttons( toolbar_element, rect, selectionbuttons );
2236
2237            });
2238
2239        };
2240
2241        var onOpenpopup = function()
2242
2243        {
2244
2245            add_class_focus();
2246
2247        };
2248
2249        var onClosepopup = function()
2250
2251        {
2252
2253            finish_suggestion();
2254
2255            remove_class_focus();
2256
2257        };
2258
2259
2260
2261        // Sync Editor with Textarea
2262
2263        var syncTextarea = null,
2264
2265            debounced_syncTextarea = null,
2266
2267            callUpdates;
2268
2269        if( node_textarea )
2270
2271        {
2272
2273            // copy placeholder from the textarea to the contenteditor
2274
2275            if( ! node_contenteditable.innerHTML && node_textarea.value )
2276
2277                node_contenteditable.innerHTML = node_textarea.value;
2278
2279
2280
2281            // sync html from the contenteditor to the textarea
2282
2283            var previous_html = node_contenteditable.innerHTML;
2284
2285            syncTextarea = function()
2286
2287            {
2288
2289                var new_html = node_contenteditable.innerHTML;
2290
2291                if( new_html.match(/^<br[/ ]*>$/i) )
2292
2293                {
2294
2295                    node_contenteditable.innerHTML = '';
2296
2297                    new_html = '';
2298
2299                }
2300
2301                if( new_html == previous_html )
2302
2303                    return;
2304
2305                // HTML changed
2306
2307                node_textarea.value = new_html;
2308
2309                previous_html = new_html;
2310
2311            };
2312
2313
2314
2315            // Focus/Blur events
2316
2317            addEvent( node_contenteditable, 'focus', function()
2318
2319            {
2320
2321                // forward focus/blur to the textarea
2322
2323                var event = document.createEvent( 'Event' );
2324
2325                event.initEvent( 'focus', false, false );
2326
2327                node_textarea.dispatchEvent( event );
2328
2329            });
2330
2331            addEvent( node_contenteditable, 'blur', function()
2332
2333            {
2334
2335                // sync textarea immediately
2336
2337                syncTextarea();
2338
2339                // forward focus/blur to the textarea
2340
2341                var event = document.createEvent( 'Event' );
2342
2343                event.initEvent( 'blur', false, false );
2344
2345                node_textarea.dispatchEvent( event );
2346
2347            });
2348
2349
2350
2351            // debounce 'syncTextarea', because 'innerHTML' is quite burdensome
2352
2353            // High timeout is save, because of "onblur" fires immediately
2354
2355            debounced_syncTextarea = debounce( syncTextarea, 250, true );
2356
2357
2358
2359            // Catch change events
2360
2361            // http://stackoverflow.com/questions/1391278/contenteditable-change-events/1411296#1411296
2362
2363            // http://stackoverflow.com/questions/8694054/onchange-event-with-contenteditable/8694125#8694125
2364
2365            // https://github.com/mindmup/bootstrap-wysiwyg/pull/50/files
2366
2367            // http://codebits.glennjones.net/editing/events-contenteditable.htm
2368
2369            addEvent( node_contenteditable, 'input', debounced_syncTextarea );
2370
2371            addEvent( node_contenteditable, 'propertychange', debounced_syncTextarea );
2372
2373            addEvent( node_contenteditable, 'textInput', debounced_syncTextarea );
2374
2375            addEvent( node_contenteditable, 'paste', debounced_syncTextarea );
2376
2377            addEvent( node_contenteditable, 'cut', debounced_syncTextarea );
2378
2379            addEvent( node_contenteditable, 'drop', debounced_syncTextarea );
2380
2381            // MutationObserver should report everything
2382
2383            if( window.MutationObserver )
2384
2385            {
2386
2387                var observer = new MutationObserver( debounced_syncTextarea );
2388
2389                observer.observe( node_contenteditable, {attributes:true,childList:true,characterData:true,subtree:true});
2390
2391            }
2392
2393
2394
2395            // handle reset event
2396
2397            var form = node_textarea.form;
2398
2399            if( form )
2400
2401            {
2402
2403                addEvent( form, 'reset', function() {
2404
2405                    node_contenteditable.innerHTML = '';
2406
2407                    debounced_syncTextarea();
2408
2409                    callUpdates( true );
2410
2411                });
2412
2413            }
2414
2415        }
2416
2417
2418
2419        // Handle selection
2420
2421        var popup_saved_selection = null, // preserve selection during popup
2422
2423            debounced_handleSelection = null;
2424
2425        if( onSelection )
2426
2427        {
2428
2429            var handleSelection = function( clientX, clientY, rightclick )
2430
2431            {
2432
2433                // Detect collapsed selection
2434
2435                var collapsed = getSelectionCollapsed( node_contenteditable );
2436
2437                // List of all selected nodes
2438
2439                var nodes = getSelectedNodes( node_contenteditable );
2440
2441                // Rectangle of the selection
2442
2443                var rect = (clientX === null || clientY === null) ? null :
2444
2445                    {
2446
2447                        left: clientX,
2448
2449                        top: clientY,
2450
2451                        width: 0,
2452
2453                        height: 0
2454
2455                    };
2456
2457                var selectionRect = getSelectionRect();
2458
2459                if( selectionRect )
2460
2461                    rect = selectionRect;
2462
2463                if( rect )
2464
2465                {
2466
2467                    // So far 'rect' is relative to viewport, make it relative to the editor
2468
2469                    var boundingrect = node_contenteditable.getBoundingClientRect();
2470
2471                    rect.left -= parseInt(boundingrect.left);
2472
2473                    rect.top -= parseInt(boundingrect.top);
2474
2475                    // Trim rectangle to the editor
2476
2477                    if( rect.left < 0 )
2478
2479                        rect.left = 0;
2480
2481                    if( rect.top < 0 )
2482
2483                        rect.top = 0;
2484
2485                    if( rect.width > node_contenteditable.offsetWidth )
2486
2487                        rect.width = node_contenteditable.offsetWidth;
2488
2489                    if( rect.height > node_contenteditable.offsetHeight )
2490
2491                        rect.height = node_contenteditable.offsetHeight;
2492
2493                }
2494
2495                else if( nodes.length )
2496
2497                {
2498
2499                    // What else could we do? Offset of first element...
2500
2501                    for( var i=0; i < nodes.length; ++i )
2502
2503                    {
2504
2505                        var node = nodes[i];
2506
2507                        if( node.nodeType != Node.ELEMENT_NODE )
2508
2509                            continue;
2510
2511                        rect = {
2512
2513                            left: node.offsetLeft,
2514
2515                            top: node.offsetTop,
2516
2517                            width: node.offsetWidth,
2518
2519                            height: node.offsetHeight
2520
2521                        };
2522
2523                        break;
2524
2525                    }
2526
2527                }
2528
2529                // Callback
2530
2531                onSelection( collapsed, rect, nodes, rightclick );
2532
2533            };
2534
2535            debounced_handleSelection = debounce( handleSelection, 1 );
2536
2537        }
2538
2539
2540
2541        // Open popup
2542
2543        var node_popup = null;
2544
2545        var popupClickClose = function( e )
2546
2547        {
2548
2549            var target = e.target || e.srcElement;
2550
2551            if( target.nodeType == Node.TEXT_NODE ) // defeat Safari bug
2552
2553                target = target.parentNode;
2554
2555            // Click within popup?
2556
2557            if( isOrContainsNode(node_popup,target) )
2558
2559                return;
2560
2561            // close popup
2562
2563            popupClose();
2564
2565        };
2566
2567        var popupOpen = function()
2568
2569        {
2570
2571            // Already open?
2572
2573            if( node_popup )
2574
2575                return node_popup;
2576
2577
2578
2579            // Global click closes popup
2580
2581            addEvent( window, 'mousedown', popupClickClose, true );
2582
2583
2584
2585            // Create popup element
2586
2587            node_popup = document.createElement( 'DIV' );
2588
2589            var parent = node_contenteditable.parentNode,
2590
2591                next = node_contenteditable.nextSibling;
2592
2593            if( next )
2594
2595                parent.insertBefore( node_popup, next );
2596
2597            else
2598
2599                parent.appendChild( node_popup );
2600
2601            if( onOpenpopup )
2602
2603                onOpenpopup();
2604
2605            return node_popup;
2606
2607        };
2608
2609        var popupClose = function()
2610
2611        {
2612
2613            if( ! node_popup )
2614
2615                return;
2616
2617            node_popup.parentNode.removeChild( node_popup );
2618
2619            node_popup = null;
2620
2621            removeEvent( window, 'mousedown', popupClickClose, true );
2622
2623            if( onClosepopup )
2624
2625                onClosepopup();
2626
2627        };
2628
2629
2630
2631        // Key events
2632
2633        // http://sandbox.thewikies.com/html5-experiments/key-events.html
2634
2635        var keyHandler = function( e, phase )
2636
2637        {
2638
2639            // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
2640
2641            // http://stackoverflow.com/questions/1444477/keycode-and-ch
2641arcode
2642
2643            // http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
2644
2645            // http://unixpapa.com/js/key.html
2646
2647            var key = e.which || e.keyCode,
2648
2649                character = String.fromCharCode(key || e.charCode),
2650
2651                shiftKey = e.shiftKey || false,
2652
2653                altKey = e.altKey || false,
2654
2655                ctrlKey = e.ctrlKey || false,
2656
2657                metaKey = e.metaKey || false;
2658
2659            if( phase == 1 )
2660
2661            {
2662
2663                // Callback
2664
2665                if( onKeyDown && onKeyDown(key, character, shiftKey, altKey, ctrlKey, metaKey) === false )
2666
2667                    cancelEvent( e ); // dismiss key
2668
2669            }
2670
2671            else if( phase == 2 )
2672
2673            {
2674
2675                // Callback
2676
2677                if( onKeyPress && onKeyPress(key, character, shiftKey, altKey, ctrlKey, metaKey) === false )
2678
2679                    cancelEvent( e ); // dismiss key
2680
2681            }
2682
2683            //else if( phase == 3 )
2684
2685            //{
2686
2687            //    // Callback
2688
2689            //    if( onKeyUp && onKeyUp(key, character, shiftKey, altKey, ctrlKey, metaKey) === false )
2690
2691            //        cancelEvent( e ); // dismiss key
2692
2693            //}
2694
2695
2696
2697            // Keys can change the selection
2698
2699            if( popup_saved_selection )
2700
2701                popup_saved_selection = saveSelection( node_contenteditable );
2702
2703            if( phase == 2 || phase == 3 )
2704
2705            {
2706
2707                if( debounced_handleSelection )
2708
2709                    debounced_handleSelection( null, null, false );
2710
2711            }
2712
2713            // Most keys can cause text-changes
2714
2715            if( phase == 2 && debounced_syncTextarea )
2716
2717            {
2718
2719                switch( key )
2720
2721                {
2722
2723                    case 33: // pageUp
2724
2725                    case 34: // pageDown
2726
2727                    case 35: // end
2728
2729                    case 36: // home
2730
2731                    case 37: // left
2732
2733                    case 38: // up
2734
2735                    case 39: // right
2736
2737                    case 40: // down
2738
2739                        // cursors do not
2740
2741                        break;
2742
2743                    default:
2744
2745                        // call change handler
2746
2747                        debounced_syncTextarea();
2748
2749                        break;
2750
2751                }
2752
2753            }
2754
2755        };
2756
2757        addEvent( node_contenteditable, 'keydown', function( e )
2758
2759        {
2760
2761            keyHandler( e, 1 );
2762
2763        });
2764
2765        addEvent( node_contenteditable, 'keypress', function( e )
2766
2767        {
2768
2769            keyHandler( e, 2 );
2770
2771        });
2772
2773        addEvent( node_contenteditable, 'keyup', function( e )
2774
2775        {
2776
2777            keyHandler( e, 3 );
2778
2779        });
2780
2781
2782
2783        // Mouse events
2784
2785        var mouseHandler = function( e, rightclick )
2786
2787        {
2788
2789            // mouse position
2790
2791            var clientX = null,
2792
2793                clientY = null;
2794
2795            if( e.clientX && e.clientY )
2796
2797            {
2798
2799                clientX = e.clientX;
2800
2801                clientY = e.clientY;
2802
2803            }
2804
2805            else if( e.pageX && e.pageY )
2806
2807            {
2808
2809                clientX = e.pageX - window.pageXOffset;
2810
2811                clientY = e.pageY - window.pageYOffset;
2812
2813            }
2814
2815            // mouse button
2816
2817            if( e.which && e.which == 3 )
2818
2819                rightclick = true;
2820
2821            else if( e.button && e.button == 2 )
2822
2823                rightclick = true;
2824
2825
2826
2827            // remove event handler
2828
2829            removeEvent( window, 'mouseup', mouseHandler );
2830
2831            // Callback selection
2832
2833            if( popup_saved_selection )
2834
2835                popup_saved_selection = saveSelection( node_contenteditable );
2836
2837            if( ! hijackContextmenu && rightclick )
2838
2839                return;
2840
2841            if( debounced_handleSelection )
2842
2843                debounced_handleSelection( clientX, clientY, rightclick );
2844
2845        };
2846
2847        var mouse_down_target = null;
2848
2849        addEvent( node_contenteditable, 'mousedown', function( e )
2850
2851        {
2852
2853            // catch event if 'mouseup' outside 'contenteditable'
2854
2855            removeEvent( window, 'mouseup', mouseHandler );
2856
2857            addEvent( window, 'mouseup', mouseHandler );
2858
2859            // remember target
2860
2861            mouse_down_target = e.target;
2862
2863        });
2864
2865        addEvent( node_contenteditable, 'mouseup', function( e )
2866
2867        {
2868
2869            // Select image (improve user experience on Webkit)
2870
2871            var node = e.target;
2872
2873            if( node && node.nodeType == Node.ELEMENT_NODE && node === mouse_down_target &&
2874
2875                isMediaNode(node) && isOrContainsNode(node_contenteditable, node, true) )
2876
2877            {
2878
2879                var selection = window.getSelection();
2880
2881                var range = document.createRange();
2882
2883                range.setStartBefore( node );
2884
2885                range.setEndAfter( node );
2886
2887                selection.removeAllRanges();
2888
2889                selection.addRange( range );
2890
2891            }
2892
2893            // handle click
2894
2895            mouseHandler( e );
2896
2897            // Trigger change
2898
2899            if( debounced_syncTextarea )
2900
2901                debounced_syncTextarea();
2902
2903        });
2904
2905        addEvent( node_contenteditable, 'dblclick', function( e )
2906
2907        {
2908
2909            mouseHandler( e );
2910
2911        });
2912
2913        addEvent( node_contenteditable, 'selectionchange',  function( e )
2914
2915        {
2916
2917            mouseHandler( e );
2918
2919        });
2920
2921        if( hijackContextmenu )
2922
2923        {
2924
2925            addEvent( node_contenteditable, 'contextmenu', function( e )
2926
2927            {
2928
2929                mouseHandler( e, true );
2930
2931                cancelEvent( e );
2932
2933            });
2934
2935        }
2936
2937
2938
2939        // exec command
2940
2941        // https://developer.mozilla.org/en-US/docs/Web/API/document.execCommand
2942
2943        // http://www.quirksmode.org/dom/execCommand.html
2944
2945        var execCommand = function( command, param, force_selection )
2946
2947        {
2948
2949            // give selection to contenteditable element
2950
2951            restoreSelection( node_contenteditable, popup_saved_selection );
2952
2953            // tried to avoid forcing focus(), but ... - https://github.com/wysiwygjs/wysiwyg.js/issues/51
2954
2955            node_contenteditable.focus();
2956
2957            if( ! selectionInside(node_contenteditable, force_selection) ) // returns 'selection inside editor'
2958
2959                return false;
2960
2961
2962
2963            // Buggy, call within 'try/catch'
2964
2965            try {
2966
2967                if( document.queryCommandSupported && ! document.queryCommandSupported(command) )
2968
2969                    return false;
2970
2971                return document.execCommand( command, false, param );
2972
2973            }
2974
2975            catch( e ) {
2976
2977            }
2978
2979            return false;
2980
2981        };
2982
2983
2984
2985        // copy/paste images from clipboard
2986
2987        addEvent( node_contenteditable, 'paste', function( e )
2988
2989        {
2990
2991            var clipboardData = e.clipboardData;
2992
2993            if( ! clipboardData )
2994
2995                return;
2996
2997            var items = clipboardData.items;
2998
2999            if( ! items || ! items.length )
3000
3001                return;
3002
3003            var item = items[0];
3004
3005            if( ! item.type.match(/^image\//) )
3006
3007                return;
3008
3009            // Insert image from clipboard
3010
3011            filecontents( item.getAsFile(), function( type, dataurl ) {
3012
3013                if( dataurl )   // empty on error
3014
3015                    execCommand( 'insertImage', dataurl );
3016
3017            });
3018
3019            cancelEvent( e ); // dismiss paste
3020
3021        });
3022
3023
3024
3025        // Command structure
3026
3027        callUpdates = function( selection_destroyed )
3028
3029        {
3030
3031            // change-handler
3032
3033            if( debounced_syncTextarea )
3034
3035                debounced_syncTextarea();
3036
3037            // handle saved selection
3038
3039            if( selection_destroyed )
3040
3041            {
3042
3043                collapseSelectionEnd();
3044
3045                popup_saved_selection = null; // selection destroyed
3046
3047            }
3048
3049            else if( popup_saved_selection )
3050
3051                popup_saved_selection = saveSelection( node_contenteditable );
3052
3053        };
3054
3055        commands = {
3056
3057            // properties
3058
3059            sync: function()
3060
3061            {
3062
3063                // sync textarea immediately
3064
3065                if( syncTextarea )
3066
3067                    syncTextarea();
3068
3069                return this;
3070
3071            },
3072
3073            getHTML: function()
3074
3075            {
3076
3077                return node_contenteditable.innerHTML;
3078
3079            },
3080
3081            setHTML: function( html )
3082
3083            {
3084
3085                node_contenteditable.innerHTML = html || '';
3086
3087                callUpdates( true ); // selection destroyed
3088
3089                return this;
3090
3091            },
3092
3093            getSelectedHTML: function()
3094
3095            {
3096
3097                restoreSelection( node_contenteditable, popup_saved_selection );
3098
3099                if( ! selectionInside(node_contenteditable) )
3100
3101                    return null;
3102
3103                return getSelectionHtml( node_contenteditable );
3104
3105            },
3106
3107            // selection and popup
3108
3109            collapseSelection: function()
3110
3111            {
3112
3113                collapseSelectionEnd();
3114
3115                popup_saved_selection = null; // selection destroyed
3116
3117                return this;
3118
3119            },
3120
3121            expandSelection: function( preceding, following )
3122
3123            {
3124
3125                restoreSelection( node_contenteditable, popup_saved_selection );
3126
3127                if( ! selectionInside(node_contenteditable) )
3128
3129                    return this;
3130
3131                expandSelectionCaret( node_contenteditable, preceding, following );
3132
3133                popup_saved_selection = saveSelection( node_contenteditable ); // save new selection
3134
3135                return this;
3136
3137            },
3138
3139            openPopup: function()
3140
3141            {
3142
3143                if( ! popup_saved_selection )
3144
3145                    popup_saved_selection = saveSelection( node_contenteditable ); // save current selection
3146
3147                return popupOpen();
3148
3149            },
3150
3151            activePopup: function()
3152
3153            {
3154
3155                return node_popup;
3156
3157            },
3158
3159            closePopup: function()
3160
3161            {
3162
3163                popupClose();
3164
3165                return this;
3166
3167            },
3168
3169            // formats
3170
3171            removeFormat: function()
3172
3173            {
3174
3175                execCommand( 'removeFormat' );
3176
3177                execCommand( 'unlink' );
3178
3179                callUpdates();
3180
3181                return this;
3182
3183            },
3184
3185            bold: function()
3186
3187            {
3188
3189                execCommand( 'bold' );
3190
3191                callUpdates();
3192
3193                return this;
3194
3195            },
3196
3197            italic: function()
3198
3199            {
3200
3201                execCommand( 'italic' );
3202
3203                callUpdates();
3204
3205                return this;
3206
3207            },
3208
3209            underline: function()
3210
3211            {
3212
3213                execCommand( 'underline' );
3214
3215                callUpdates();
3216
3217                return this;
3218
3219            },
3220
3221            strikethrough: function()
3222
3223            {
3224
3225                execCommand( 'strikeThrough' );
3226
3227                callUpdates();
3228
3229                return this;
3230
3231            },
3232
3233            forecolor: function( color )
3234
3235            {
3236
3237                execCommand( 'foreColor', color );
3238
3239                callUpdates();
3240
3241                return this;
3242
3243            },
3244
3245            highlight: function( color )
3246
3247            {
3248
3249                // http://stackoverflow.com/questions/2756931/highlight-the-text-of-the-dom-range-element
3250
3251                if( ! execCommand('hiliteColor',color) ) // some browsers apply 'backColor' to the whole block
3252
3253                    execCommand( 'backColor', color );
3254
3255                callUpdates();
3256
3257                return this;
3258
3259            },
3260
3261            fontName: function( name )
3262
3263            {
3264
3265                execCommand( 'fontName', name );
3266
3267                callUpdates();
3268
3269                return this;
3270
3271            },
3272
3273            fontSize: function( size )
3274
3275            {
3276
3277                execCommand( 'fontSize', size );
3278
3279                callUpdates();
3280
3281                return this;
3282
3283            },
3284
3285            subscript: function()
3286
3287            {
3288
3289                execCommand( 'subscript' );
3290
3291                callUpdates();
3292
3293                return this;
3294
3295            },
3296
3297            superscript: function()
3298
3299            {
3300
3301                execCommand( 'superscript' );
3302
3303                callUpdates();
3304
3305                return this;
3306
3307            },
3308
3309            insertLink: function( url )
3310
3311            {
3312
3313                execCommand( 'createLink', url );
3314
3315                callUpdates( true ); // selection destroyed
3316
3317                return this;
3318
3319            },
3320
3321            insertImage: function( url )
3322
3323            {
3324
3325                execCommand( 'insertImage', url, true );
3326
3327                callUpdates( true ); // selection destroyed
3328
3329                return this;
3330
3331            },
3332
3333            insertHTML: function( html )
3334
3335            {
3336
3337                if( ! execCommand('insertHTML', html, true) )
3338
3339                {
3340
3341                    // IE 11 still does not support 'insertHTML'
3342
3343                    restoreSelection( node_contenteditable, popup_saved_selection );
3344
3345                    selectionInside( node_contenteditable, true );
3346
3347                    pasteHtmlAtCaret( node_contenteditable, html );
3348
3349                }
3350
3351                callUpdates( true ); // selection destroyed
3352
3353                return this;
3354
3355            },
3356
3357            orderedList: function()
3358
3359            {
3360
3361                execCommand( 'insertOrderedList' );
3362
3363                callUpdates();
3364
3365                return this;
3366
3367            },
3368
3369            unorderedList: function()
3370
3371            {
3372
3373                execCommand( 'insertUnorderedList' );
3374
3375                callUpdates();
3376
3377                return this;
3378
3379            },
3380
3381        };
3382
3383
3384
3385        // Create toolbar
3386
3387        if( buttons )
3388
3389        {
3390
3391            var toolbar_element = document.createElement('div');
3392
3393            add_class( toolbar_element, 'toolbar' );
3394
3395            if( toolbar_top )
3396
3397            {
3398
3399                add_class( toolbar_element, 'toolbar-top' );
3400
3401                node_container.insertBefore( toolbar_element, node_container.firstChild );
3402
3403            }
3404
3405            else if( toolbar_bottom )
3406
3407            {
3408
3409                add_class( toolbar_element, 'toolbar-bottom' );
3410
3411                node_container.appendChild( toolbar_element );
3412
3413            }
3414
3415            else
3416
3417            {
3418
3419                var toolbar_wrapper = document.createElement('div');
3420
3421                add_class( toolbar_wrapper, 'toolbar-auto' );
3422
3423                node_container.appendChild( toolbar_wrapper );
3424
3425                toolbar_wrapper.appendChild( toolbar_element );
3426
3427            }
3428
3429            fill_buttons( toolbar_element, null, buttons, hotkeys );
3430
3431        }
3432
3433
3434
3435        return commands;
3436
3437    };
3438
3439
3440
3441    // https://www.matteoagosti.com/blog/2013/02/24/writing-javascript-modules-for-both-browser-and-node/
3442
3443    if( typeof module !== 'undefined' && typeof module.exports !== 'undefined' )
3444
3445        module.exports = wysiwyg;
3446
3447    else
3448
3449        window.wysiwyg = wysiwyg;
3450
3451})(window, document);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.