PageSourceSearch

https://webukatu.com/assets/js/redactor/redactor.js?1633567422

js webukatu.com collected 2026-10-02 06:16:45 UTC 523,232 bytes, 18,664 lines download raw bytes

1/*
2    Redactor
3    Version 3.5.2
4    Updated: October 7, 2021
5
6    http://imperavi.com/redactor/
7
8    Copyright (c) 2009-2021, Imperavi Ltd.
9    License: http://imperavi.com/redactor/license/
10*/
11if (typeof CodeMirror === 'undefined') { var CodeMirror = undefined; }
12if (typeof jQuery === 'undefined') { var jQuery = undefined; }
13(function() {
14var Ajax = {};
15
16Ajax.settings = {};
17Ajax.post = function(options) { return new AjaxRequest('post', options); };
18Ajax.get = function(options) { return new AjaxRequest('get', options); };
19Ajax.request = function(method, options) { return new AjaxRequest(method, options); };
20
21var AjaxRequest = function(method, options)
22{
23    var defaults = {
24        method: method,
25        url: '',
26        before: function() {},
27        success: function() {},
28        error: function() {},
29        data: false,
30        async: true,
31        headers: {}
32    };
33
34    this.p = this.extend(defaults, options);
35    this.p = this.extend(this.p, Ajax.settings);
36    this.p.method = this.p.method.toUpperCase();
37
38    this.prepareData();
39
40    this.xhr = new XMLHttpRequest();
41    this.xhr.open(this.p.method, this.p.url, this.p.async);
42
43    this.setHeaders();
44
45    var before = (typeof this.p.before === 'function') ? this.p.before(this.xhr) : true;
46    if (before !== false)
47    {
48        this.send();
49    }
50};
51
52AjaxRequest.prototype = {
53    extend: function(obj1, obj2)
54    {
55        if (obj2) for (var name in obj2) { obj1[name] = obj2[name]; }
56        return obj1;
57    },
58    prepareData: function()
59    {
60        if (['POST', 'PUT'].indexOf(this.p.method) !== -1 && !this.isFormData()) this.p.headers['Content-Type'] = 'application/x-www-form-urlencoded';
61        if (typeof this.p.data === 'object' && !this.isFormData()) this.p.data = this.toParams(this.p.data);
62        if (this.p.method === 'GET') this.p.url = (this.p.data) ? this.p.url + '?' + this.p.data : this.p.url;
63    },
64    setHeaders: function()
65    {
66        this.xhr.setRequestHeader('X-Requested-With', this.p.headers['X-Requested-With'] || 'XMLHttpRequest');
67        for (var name in this.p.headers)
68        {
69            this.xhr.setRequestHeader(name, this.p.headers[name]);
70        }
71    },
72    isFormData: function()
73    {
74        return (typeof window.FormData !== 'undefined' && this.p.data instanceof window.FormData);
75    },
76    isComplete: function()
77    {
78        return !(this.xhr.status < 200 || (this.xhr.status >= 300 && this.xhr.status !== 304));
79    },
80    send: function()
81    {
82        if (this.p.async)
83        {
84            this.xhr.onload = this.loaded.bind(this);
85            this.xhr.send(this.p.data);
86        }
87        else
88        {
89            this.xhr.send(this.p.data);
90            this.loaded.call(this);
91        }
92    },
93    loaded: function()
94    {
95        if (this.isComplete())
96        {
97            var response = this.parseResponse();
98            if (typeof this.p.success === 'function') this.p.success(response, this.xhr);
99        }
100        else
101        {
102            var response = this.parseResponse();
103            if (typeof this.p.error === 'function') this.p.error(response, this.xhr, this.xhr.status);
104        }
105    },
106    parseResponse: function() {
107        var response = this.xhr.response;
108        var json = this.parseJson(response);
109        return (json) ? json : response;
110    },
111    parseJson: function(str)
112    {
113        try {
114            var o = JSON.parse(str);
115            if (o && typeof o === 'object')
116            {
117                return o;
118            }
119
120        } catch (e) {}
121
122        return false;
123    },
124    toParams: function (obj)
125    {
126        return Object.keys(obj).map(
127            function(k){ return encodeURIComponent(k) + '=' + encodeURIComponent(obj[k]); }
128        ).join('&');
129    }
130};
131var DomCache = [0];
132var DomExpando = 'data' + new Date().getTime();
133var DomHClass = 'is-hidden';
134var DomHMClass = 'is-hidden-mobile';
135
136var Dom = function(selector, context)
137{
138    return this.parse(selector, context);
139};
140
141Dom.ready = function(fn)
142{
143    if (document.readyState !== 'loading') fn();
144    else document.addEventListener('DOMContentLoaded', fn);
145};
146
147Dom.prototype = {
148    get sdom()
149    {
150        return true;
151    },
152    get length()
153    {
154        return this.nodes.length;
155    },
156    parse: function(selector, context)
157    {
158        var nodes;
159        var reHtmlTest = /^\s*<(\w+|!)[^>]*>/;
160
161        if (!selector)
162        {
163            nodes = [];
164        }
165        else if (selector.sdom)
166        {
167            this.nodes = selector.nodes;
168            return selector;
169        }
170        else if (typeof selector !== 'string')
171        {
172            if (selector.nodeType && selector.nodeType === 11)
173            {
174                nodes = selector.childNodes;
175            }
176            else
177            {
178                nodes = (selector.nodeType || selector === window) ? [selector] : selector;
179            }
180        }
181        else if (reHtmlTest.test(selector))
182        {
183            nodes = this.create(selector);
184        }
185        else
186        {
187            nodes = this._query(selector, context);
188        }
189
190        this.nodes = this._slice(nodes);
191    },
192    create: function(html)
193    {
194        if (/^<(\w+)\s*\/?>(?:<\/\1>|)$/.test(html))
195        {
196            return [document.createElement(RegExp.$1)];
197        }
198
199        var elements = [];
200        var container = document.createElement('div');
201        var children = container.childNodes;
202
203        container.innerHTML = html;
204
205        for (var i = 0, l = children.length; i < l; i++)
206        {
207            elements.push(children[i]);
208        }
209
210        return elements;
211    },
212
213    // add
214    add: function(nodes)
215    {
216        this.nodes = this.nodes.concat(this._toArray(nodes));
217    },
218
219    // get
220    get: function(index)
221    {
222        return this.nodes[(index || 0)] || false;
223    },
224    getAll: function()
225    {
226        return this.nodes;
227    },
228    eq: function(index)
229    {
230        return new Dom(this.nodes[index]);
231    },
232    first: function()
233    {
234        return new Dom(this.nodes[0]);
235    },
236    last: function()
237    {
238        return new Dom(this.nodes[this.nodes.length - 1]);
239    },
240    contents: function()
241    {
242        return this.get().childNodes;
243    },
244
245    // loop
246    each: function(callback)
247    {
248        var len = this.nodes.length;
249        for (var i = 0; i < len; i++)
250        {
251            callback.call(this, (this.nodes[i].sdom) ? this.nodes[i].get() : this.nodes[i], i);
252        }
253
254        return this;
255    },
256
257    // traversing
258    is: function(selector)
259    {
260        return (this.filter(selector).length > 0);
261    },
262    filter: function (selector)
263    {
264        var callback;
265        if (selector === undefined)
266        {
267            return this;
268        }
269        else if (typeof selector === 'function')
270        {
271            callback = selector;
272        }
273        else
274        {
275            callback = function(node)
276            {
277                if (selector instanceof Node)
278                {
279                    return (selector === node);
280                }
281                else if (selector && selector.sdom)
282                {
283                    return ((selector.nodes).indexOf(node) !== -1);
284                }
285                else
286                {
287                    node.matches = node.matches || node.msMatchesSelector || node.webkitMatchesSelector;
288                    return (node.nodeType === 1) ? node.matches(selector || '*') : false;
289                }
290            };
291        }
292
293        return new Dom(this.nodes.filter(callback));
294    },
295    not: function(filter)
296    {
297        return this.filter(function(node)
298        {
299            return !new Dom(node).is(filter || true);
300        });
301    },
302    find: function(selector)
303    {
304        var nodes = [];
305        this.each(function(node)
306        {
307            var ns = this._query(selector || '*', node);
308            for (var i = 0; i < ns.length; i++)
309            {
310                nodes.push(ns[i]);
311            }
312        });
313
314        return new Dom(nodes);
315    },
316    children: function(selector)
317    {
318        var nodes = [];
319        this.each(function(node)
320        {
321            if (node.children)
322            {
323                var ns = node.children;
324                for (var i = 0; i < ns.length; i++)
325                {
326                    nodes.push(ns[i]);
327                }
328            }
329        });
330
331        return new Dom(nodes).filter(selector);
332    },
333    parent: function(selector)
334    {
335        var nodes = [];
336        this.each(function(node)
337        {
338            if (node.parentNode) nodes.push(node.parentNode);
339        });
340
341        return new Dom(nodes).filter(selector);
342    },
343    parents: function(selector, context)
344    {
345        context = this._getContext(context);
346
347        var nodes = [];
348        this.each(function(node) {
349            var parent = node.parentNode;
350            while (parent && parent !== context) {
351                if (selector) {
352                    if (new Dom(parent).is(selector)) { nodes.push(parent); }
353                }
354                else {
355                    nodes.push(parent);
356                }
357                parent = parent.parentNode;
358            }
359        });
360
361        return new Dom(nodes);
362
363    },
364    closest: function(selector, context)
365    {
366        context = this._getContext(context);
367        selector = (selector.sdom) ? selector.get() : selector;
368
369        var nodes = [];
370        var isNode = (selector && selector.nodeType);
371        this.each(function(node)
372        {
373            do {
374                if ((isNode && node === selector) || new Dom(node).is(selector)) return nodes.push(node);
375            } while ((node = node.parentNode) && node !== context);
376        });
377
378        return new Dom(nodes);
379    },
380    next: function(selector)
381    {
382         return this._getSibling(selector, 'nextSibling');
383    },
384    nextElement: function(selector)
385    {
386        return this._getSibling(selector, 'nextElementSibling');
387    },
388    prev: function(selector)
389    {
390        return this._getSibling(selector, 'previousSibling');
391    },
392    prevElement: function(selector)
393    {
394        return this._getSibling(selector, 'previousElementSibling');
395    },
396
397    // css
398    css: function(name, value)
399    {
400        if (value === undefined && (typeof name !== 'object'))
401        {
402            var node = this.get();
403            if (name === 'width' || name === 'height')
404            {
405                return (node.style) ? this._getHeightOrWidth(name, node, false) + 'px' : undefined;
406            }
407            else
408            {
409                return (node.style) ? getComputedStyle(node, null)[name] : undefined;
410            }
411        }
412
413        // set
414        return this.each(function(node)
415        {
416            var obj = {};
417            if (typeof name === 'object') obj = name;
418            else obj[name] = value;
419
420            for (var key in obj)
421            {
422                if (node.style) node.style[key] = obj[key];
423            }
424        });
425    },
426
427    // attr
428    attr: function(name, value, data)
429    {
430        data = (data) ? 'data-' : '';
431
432        if (value === undefined && (typeof name !== 'object'))
433        {
434            var node = this.get();
435            if (node && node.nodeType !== 3)
436            {
437                return (name === 'checked') ? node.checked : this._getBooleanFromStr(node.getAttribute(data + name));
438            }
439            else return;
440        }
441
442        // set
443        return this.each(function(node)
444        {
445            var obj = {};
446            if (typeof name === 'object') obj = name;
447            else obj[name] = value;
448
449            for (var key in obj)
450            {
451                if (node.nodeType !== 3)
452                {
453                    if (key === 'checked') node.checked = obj[key];
454                    else node.setAttribute(data + key, obj[key]);
455                }
456            }
457        });
458    },
459    data: function(name, value)
460    {
461        if (name === undefined)
462        {
463            var reDataAttr = /^data-(.+)$/;
464            var attrs = this.get().attributes;
465
466            var data = {};
467            var replacer = function (g) { return g[1].toUpperCase(); };
468
469            for (var key in attrs)
470            {
471                if (attrs[key] && reDataAttr.test(attrs[key].nodeName))
472                {
473                    var dataName = attrs[key].nodeName.match(reDataAttr)[1];
474                    var val = attrs[key].value;
475                    dataName = dataName.replace(/-([a-z])/g, replacer);
476
477                    if (this._isObjectString(val)) val = this._toObject(val);
478                    else val = (this._isNumber(val)) ? parseFloat(val) : this._getBooleanFromStr(val);
479
480                    data[dataName] = val;
481                }
482            }
483
484            return data;
485        }
486
487        return this.attr(name, value, true);
488    },
489    val: function(value)
490    {
491        if (value === undefined)
492        {
493            var el = this.get();
494            if (el.type && el.type === 'checkbox') return el.checked;
495            else return el.value;
496        }
497
498        return this.each(function(node)
499        {
500            node.value = value;
501        });
502    },
503    removeAttr: function(value)
504    {
505        return this.each(function(node)
506        {
507            var rmAttr = function(name) { if (node.nodeType !== 3) node.removeAttribute(name); };
508            value.split(' ').forEach(rmAttr);
509        });
510    },
511    removeData: function(value)
512    {
513        return this.each(function(node)
514        {
515            var rmData = function(name) { if (node.nodeType !== 3) node.removeAttribute('data-' + name); };
516            value.split(' ').forEach(rmData);
517        });
518    },
519
520    // dataset/dataget
521    dataset: function(key, value)
522    {
523        return this.each(function(node)
524        {
525            DomCache[this.dataindex(node)][key] = value;
526        });
527    },
528    dataget: function(key)
529    {
530        return DomCache[this.dataindex(this.get())][key];
531    },
532    dataindex: function(el)
533    {
534        var cacheIndex = el[DomExpando];
535        var nextCacheIndex = DomCache.length;
536
537        if (!cacheIndex)
538        {
539            cacheIndex = el[DomExpando] = nextCacheIndex;
540            DomCache[cacheIndex] = {};
541        }
542
543        return cacheIndex;
544    },
545
546
547    // class
548    addClass: function(value)
549    {
550        return this._eachClass(value, 'add');
551    },
552    removeClass: function(value)
553    {
554        return this._eachClass(value, 'remove');
555    },
556    toggleClass: function(value)
557    {
558        return this._eachClass(value, 'toggle');
559    },
560    hasClass: function(value)
561    {
562        return this.nodes.some(function(node)
563        {
564            return (node.classList) ? node.classList.contains(value) : false;
565        });
566    },
567
568    // html & text
569    empty: function()
570    {
571        return this.each(function(node)
572        {
573            node.innerHTML = '';
574        });
575    },
576    html: function(html)
577    {
578        return (html === undefined) ? (this.get().innerHTML || '') : this.empty().append(html);
579    },
580    text: function(text)
581    {
582        return (text === undefined) ? (this.get().textContent || '') : this.each(function(node) { node.textContent = text; });
583    },
584
585    // manipulation
586    after: function(html)
587    {
588        return this._inject(html, function(frag, node)
589        {
590            if (typeof frag === 'string')
591            {
592                node.insertAdjacentHTML('afterend', frag);
593            }
594            else
595            {
596                if (node.parentNode !== null) {
597                    for (var i = frag instanceof Node ? [frag] : this._toArray(frag).reverse(), s = 0; s < i.length; s++) {
598                        node.parentNode.insertBefore(i[s], node.nextSibling);
599                    }
600                }
601            }
602
603            return node;
604
605        });
606    },
607    before: function(html)
608    {
609        return this._inject(html, function(frag, node)
610        {
611            if (typeof frag === 'string')
612            {
613                node.insertAdjacentHTML('beforebegin', frag);
614            }
615            else
616            {
617                var elms = (frag instanceof Node) ? [frag] : this._toArray(frag);
618                for (var i = 0; i < elms.length; i++)
619                {
620                    node.parentNode.insertBefore(elms[i], node);
621                }
622            }
623
624            return node;
625        });
626    },
627    append: function(html)
628    {
629        return this._inject(html, function(frag, node)
630        {
631            if (typeof frag === 'string' || typeof frag === 'number')
632            {
633                node.insertAdjacentHTML('beforeend', frag);
634            }
635            else
636            {
637                var elms = (frag instanceof Node) ? [frag] : this._toArray(frag);
638                for (var i = 0; i < elms.length; i++)
639                {
640                    node.appendChild(elms[i]);
641                }
642            }
643
644            return node;
645        });
646    },
647    prepend: function(html)
648    {
649        return this._inject(html, function(frag, node)
650        {
651            if (typeof frag === 'string' || typeof frag === 'number')
652            {
653                node.insertAdjacentHTML('afterbegin', frag);
654            }
655            else
656            {
657                var elms = (frag instanceof Node) ? [frag] : this._toArray(frag).reverse();
658                for (var i = 0; i < elms.length; i++)
659                {
660                    node.insertBefore(elms[i], node.firstChild);
661                }
662            }
663
664            return node;
665        });
666    },
667    wrap: function(html)
668    {
669        return this._inject(html, function(frag, node)
670        {
671            var wrapper = (typeof frag === 'string' || typeof frag === 'number') ? this.create(frag)[0] : (frag instanceof Node) ? frag : this._toArray(frag)[0];
672
673            if (node.parentNode)
674            {
675                node.parentNode.insertBefore(wrapper, node);
676            }
677
678            wrapper.appendChild(node);
679
680            return new Dom(wrapper);
681
682        });
683    },
684    unwrap: function()
685    {
686        return this.each(function(node)
687        {
688            var $node = new Dom(node);
689
690            return $node.replaceWith($node.contents());
691        });
692    },
693    replaceWith: function(html)
694    {
695        return this._inject(html, function(frag, node)
696        {
697            var docFrag = document.createDocumentFragment();
698            var elms = (typeof frag === 'string' || typeof frag === 'number') ? this.create(frag) : (frag instanceof Node) ? [frag] : this._toArray(frag);
699
700            for (var i = 0; i < elms.length; i++)
701            {
702                docFrag.appendChild(elms[i]);
703            }
704
705            var result = docFrag.childNodes[0];
706            if (node.parentNode) {
707                node.parentNode.replaceChild(docFrag, node);
708            }
709
710            return result;
711
712        });
713    },
714    remove: function()
715    {
716        return this.each(function(node)
717        {
718            if (node.parentNode) node.parentNode.removeChild(node);
719        });
720    },
721    clone: function(events)
722    {
723        var nodes = [];
724        this.each(function(node)
725        {
726            var copy = this._clone(node);
727            if (events) copy = this._cloneEvents(node, copy);
728            nodes.push(copy);
729        });
730
731        return new Dom(nodes);
732    },
733
734    // show/hide
735    show: function()
736    {
737        return this.each(function(node)
738        {
739            if (!node.style || !this._hasDisplayNone(node)) return;
740
741            var target = node.getAttribute('domTargetShow');
742            var isHidden = (node.classList) ? node.classList.contains(DomHClass) : false;
743            var isHiddenMobile = (node.classList) ? node.classList.contains(DomHMClass) : false;
744            var type;
745
746            if (isHidden)
747            {
748                type = DomHClass;
749                node.classList.remove(DomHClass);
750            }
751            else if (isHiddenMobile)
752            {
753                type = DomHMClass;
754                node.classList.remove(DomHMClass);
755            }
756            else
757            {
758                node.style.display = (target) ? target : 'block';
759            }
760
761            if (type) node.setAttribute('domTargetHide', type);
762            node.removeAttribute('domTargetShow');
763
764        }.bind(this));
765    },
766    hide: function()
767    {
768        return this.each(function(node)
769        {
770            if (!node.style || this._hasDisplayNone(node)) return;
771
772            var display = node.style.display;
773            var target = node.getAttribute('domTargetHide');
774
775            if (target === DomHClass)
776            {
777                node.classList.add(DomHClass);
778            }
779            else if (target === DomHMClass)
780            {
781                node.classList.add(DomHMClass);
782            }
783            else
784            {
785                if (display !== 'block') node.setAttribute('domTargetShow', display);
786                node.style.display = 'none';
787            }
788
789            node.removeAttribute('domTargetHide');
790
791        });
792    },
793
794    // dimensions
795    scrollTop: function(value)
796    {
797        var node = this.get();
798        var isWindow = (node === window);
799        var isDocument = (node.nodeType === 9);
800        var el = (isDocument) ? (document.scrollingElement || document.body.parentNode || document.body || document.documentElement) : node;
801
802        if (value !== undefined)
803        {
804            if (isWindow) window.scrollTo(0, value);
805            else el.scrollTop = value;
806            return;
807        }
808
809        if (isDocument)
810        {
811            return (typeof window.pageYOffset != 'undefined') ? window.pageYOffset : ((document.documentElement.scrollTop) ? document.documentElement.scrollTop : ((document.body.scrollTop) ? document.body.scrollTop : 0));
812        }
813        else
814        {
815            return (isWindow) ? window.pageYOffset : el.scrollTop;
816        }
817    },
818    offset: function()
819    {
820        return this._getDim('Offset');
821    },
822    position: function()
823    {
824        return this._getDim('Position');
825    },
826    width: function(value, adjust)
827    {
828        return this._getSize('width', 'Width', value, adjust);
829    },
830    height: function(value, adjust)
831    {
832        return this._getSize('height', 'Height', value, adjust);
833    },
834    outerWidth: function()
835    {
836        return this._getInnerOrOuter('width', 'outer');
837    },
838    outerHeight: function()
839    {
840        return this._getInnerOrOuter('height', 'outer');
841    },
842    innerWidth: function()
843    {
844        return this._getInnerOrOuter('width', 'inner');
845    },
846    innerHeight: function()
847    {
848        return this._getInnerOrOuter('height', 'inner');
849    },
850
851    // events
852    click: function()
853    {
854        return this._triggerEvent('click');
855    },
856    focus: function()
857    {
858        return this._triggerEvent('focus');
859    },
860    trigger: function(names)
861    {
862        return this.each(function(node)
863        {
864            var events = names.split(' ');
865            for (var i = 0; i < events.length; i++)
866            {
867                var ev;
868                var opts = { bubbles: true, cancelable: true };
869
870                try {
871                    ev = new window.CustomEvent(events[i], opts);
872                } catch(e) {
873                    ev = document.createEvent('CustomEvent');
874                    ev.initCustomEvent(events[i], true, true);
875                }
876
877                node.dispatchEvent(ev);
878            }
879        });
880    },
881    on: function(names, handler, one)
882    {
883        return this.each(function(node)
884        {
885            var events = names.split(' ');
886            for (var i = 0; i < events.length; i++)
887            {
888                var event = this._getEventName(events[i]);
889                var namespace = this._getEventNamespace(events[i]);
890
891                handler = (one) ? this._getOneHandler(handler, names) : handler;
892                node.addEventListener(event, handler);
893
894                node._e = node._e || {};
895                node._e[namespace] = node._e[namespace] || {};
896                node._e[namespace][event] = node._e[namespace][event] || [];
897                node._e[namespace][event].push(handler);
898            }
899
900        });
901    },
902    one: function(events, handler)
903    {
904        return this.on(events, handler, true);
905    },
906    off: function(names, handler)
907    {
908        var testEvent = function(name, key, event) { return (name === event); };
909        var testNamespace = function(name, key, event, namespace) { return (key === namespace); };
910        var testEventNamespace = function(name, key, event, namespace) { return (name === event && 
910key === namespace); };
911        var testPositive = function() { return true; };
912
913        if (names === undefined)
914        {
915            // ALL
916            return this.each(function(node)
917            {
918                this._offEvent(node, false, false, handler, testPositive);
919            });
920        }
921
922        return this.each(function(node)
923        {
924            var events = names.split(' ');
925
926            for (var i = 0; i < events.length; i++)
927            {
928                var event = this._getEventName(events[i]);
929                var namespace = this._getEventNamespace(events[i]);
930
931                // 1) event without namespace
932                if (namespace === '_events') this._offEvent(node, event, namespace, handler, testEvent);
933                // 2) only namespace
934                else if (!event && namespace !== '_events') this._offEvent(node, event, namespace, handler, testNamespace);
935                // 3) event + namespace
936                else this._offEvent(node, event, namespace, handler, testEventNamespace);
937            }
938        });
939    },
940
941    // form
942    serialize: function(asObject)
943    {
944        var obj = {};
945        var elms = this.get().elements;
946        for (var i = 0; i < elms.length; i++)
947        {
948            var el = elms[i];
949            if (/(checkbox|radio)/.test(el.type) && !el.checked) continue;
950            if (!el.name || el.disabled || el.type === 'file') continue;
951
952            if (el.type === 'select-multiple')
953            {
954                for (var z = 0; z < el.options.length; z++)
955                {
956                    var opt = el.options[z];
957                    if (opt.selected) obj[el.name] = opt.value;
958                }
959            }
960
961            obj[el.name] = (this._isNumber(el.value)) ? parseFloat(el.value) : this._getBooleanFromStr(el.value);
962        }
963
964        return (asObject) ? obj : this._toParams(obj);
965    },
966    ajax: function(success, error)
967    {
968        if (typeof AjaxRequest !== 'undefined')
969        {
970            var method = this.attr('method') || 'post';
971            var options = {
972                url: this.attr('action'),
973                data: this.serialize(),
974                success: success,
975                error: error
976            };
977
978            return new AjaxRequest(method, options);
979        }
980    },
981
982    // private
983    _queryContext: function(selector, context)
984    {
985        context = this._getContext(context);
986
987        return (context.nodeType !== 3 && typeof context.querySelectorAll === 'function') ? context.querySelectorAll(selector) : [];
988    },
989    _query: function(selector, context)
990    {
991        if (context)
992        {
993            return this._queryContext(selector, context);
994        }
995        else if (/^[.#]?[\w-]*$/.test(selector))
996        {
997            if (selector[0] === '#')
998            {
999                var element = document.getElementById(selector.slice(1));
1000                return element ? [element] : [];
1001            }
1002
1003            if (selector[0] === '.')
1004            {
1005                return document.getElementsByClassName(selector.slice(1));
1006            }
1007
1008            return document.getElementsByTagName(selector);
1009        }
1010
1011        return document.querySelectorAll(selector);
1012    },
1013    _getContext: function(context)
1014    {
1015        context = (typeof context === 'string') ? document.querySelector(context) : context;
1016
1017        return (context && context.sdom) ? context.get() : (context || document);
1018    },
1019    _inject: function(html, fn)
1020    {
1021        var len = this.nodes.length;
1022        var nodes = [];
1023        while (len--)
1024        {
1025            var res = (typeof html === 'function') ? html.call(this, this.nodes[len]) : html;
1026            var el = (len === 0) ? res : this._clone(res);
1027            var node = fn.call(this, el, this.nodes[len]);
1028
1029            if (node)
1030            {
1031                if (node.sdom) nodes.push(node.get());
1032                else nodes.push(node);
1033            }
1034        }
1035
1036        return new Dom(nodes);
1037    },
1038    _cloneEvents: function(node, copy)
1039    {
1040        var events = node._e;
1041        if (events)
1042        {
1043            copy._e = events;
1044            for (var name in events._events)
1045            {
1046                for (var i = 0; i < events._events[name].length; i++)
1047                {
1048                    copy.addEventListener(name, events._events[name][i]);
1049                }
1050            }
1051        }
1052
1053        return copy;
1054    },
1055    _clone: function(node)
1056    {
1057        if (typeof node === 'undefined') return;
1058        if (typeof node === 'string') return node;
1059        else if (node instanceof Node || node.nodeType) return node.cloneNode(true);
1060        else if ('length' in node)
1061        {
1062            return [].map.call(this._toArray(node), function(el) { return el.cloneNode(true); });
1063        }
1064    },
1065    _slice: function(obj)
1066    {
1067        return (!obj || obj.length === 0) ? [] : (obj.length) ? [].slice.call(obj.nodes || obj) : [obj];
1068    },
1069    _eachClass: function(value, type)
1070    {
1071        return this.each(function(node)
1072        {
1073            if (value)
1074            {
1075                var setClass = function(name) { if (node.classList) node.classList[type](name); };
1076                value.split(' ').forEach(setClass);
1077            }
1078        });
1079    },
1080    _triggerEvent: function(name)
1081    {
1082        var node = this.get();
1083        if (node && node.nodeType !== 3) node[name]();
1084        return this;
1085    },
1086    _getOneHandler: function(handler, events)
1087    {
1088        var self = this;
1089        return function()
1090        {
1091            handler.apply(this, arguments);
1092            self.off(events);
1093        };
1094    },
1095    _getEventNamespace: function(event)
1096    {
1097        var arr = event.split('.');
1098        var namespace = (arr[1]) ? arr[1] : '_events';
1099        return (arr[2]) ? namespace + arr[2] : namespace;
1100    },
1101    _getEventName: function(event)
1102    {
1103        return event.split('.')[0];
1104    },
1105    _offEvent: function(node, event, namespace, handler, condition)
1106    {
1107        for (var key in node._e)
1108        {
1109            for (var name in node._e[key])
1110            {
1111                if (condition(name, key, event, namespace))
1112                {
1113                    var handlers = node._e[key][name];
1114                    for (var i = 0; i < handlers.length; i++)
1115                    {
1116                        if (typeof handler !== 'undefined' && handlers[i].toString() !== handler.toString())
1117                        {
1118                            continue;
1119                        }
1120
1121                        node.removeEventListener(name, handlers[i]);
1122                        node._e[key][name].splice(i, 1);
1123
1124                        if (node._e[key][name].length === 0) delete node._e[key][name];
1125                        if (Object.keys(node._e[key]).length === 0) delete node._e[key];
1126                    }
1127                }
1128            }
1129        }
1130    },
1131    _getInnerOrOuter: function(method, type)
1132    {
1133        return this[method](undefined, type);
1134    },
1135    _getDocSize: function(node, type)
1136    {
1137        var body = node.body, html = node.documentElement;
1138        return Math.max(body['scroll' + type], body['offset' + type], html['client' + type], html['scroll' + type], html['offset' + type]);
1139    },
1140    _getSize: function(type, captype, value, adjust)
1141    {
1142        if (value === undefined)
1143        {
1144            var el = this.get();
1145            if (el.nodeType === 3)      value = 0;
1146            else if (el.nodeType === 9) value = this._getDocSize(el, captype);
1147            else if (el === window)     value = window['inner' + captype];
1148            else                        value = this._getHeightOrWidth(type, el, adjust || 'normal');
1149
1150            return Math.round(value);
1151        }
1152
1153        return this.each(function(node)
1154        {
1155            value = parseFloat(value);
1156            value = value + this._adjustResultHeightOrWidth(type, node, adjust || 'normal');
1157
1158            new Dom(node).css(type, value + 'px');
1159
1160        }.bind(this));
1161    },
1162    _getHeightOrWidth: function(type, el, adjust)
1163    {
1164        if (!el) return 0;
1165
1166        var name = type.charAt(0).toUpperCase() + type.slice(1);
1167        var result = 0;
1168        var style = getComputedStyle(el, null);
1169        var $el = new Dom(el);
1170        var $targets = $el.parents().filter(function(node)
1171        {
1172            return (node.nodeType === 1 && getComputedStyle(node, null).display === 'none') ? node : false;
1173        });
1174
1175        if (style.display === 'none') $targets.add(el);
1176        if ($targets.length !== 0)
1177        {
1178            var fixStyle = 'visibility: hidden !important; display: block !important;';
1179            var tmp = [];
1180
1181            $targets.each(function(node)
1182            {
1183                var $node = new Dom(node);
1184                var thisStyle = $node.attr('style');
1185                if (thisStyle !== null) tmp.push(thisStyle);
1186                $node.attr('style', (thisStyle !== null) ? thisStyle + ';' + fixStyle : fixStyle);
1187            });
1188
1189            result = $el.get()['offset' + name] - this._adjustResultHeightOrWidth(type, el, adjust);
1190
1191            $targets.each(function(node, i)
1192            {
1193                var $node = new Dom(node);
1194                if (tmp[i] === undefined) $node.removeAttr('style');
1195                else $node.attr('style', tmp[i]);
1196            });
1197        }
1198        else
1199        {
1200            result = el['offset' + name] - this._adjustResultHeightOrWidth(type, el, adjust);
1201        }
1202
1203        return result;
1204    },
1205    _adjustResultHeightOrWidth: function(type, el, adjust)
1206    {
1207        if (!el || adjust === false) return 0;
1208
1209        var fix = 0;
1210        var style = getComputedStyle(el, null);
1211        var isBorderBox = (style.boxSizing === "border-box");
1212
1213        if (type === 'height')
1214        {
1215            if (adjust === 'inner' || (adjust === 'normal' && isBorderBox))
1216            {
1217                fix += (parseFloat(style.borderTopWidth) || 0) + (parseFloat(style.borderBottomWidth) || 0);
1218            }
1219
1220            if (adjust === 'outer') fix -= (parseFloat(style.marginTop) || 0) + (parseFloat(style.marginBottom) || 0);
1221        }
1222        else
1223        {
1224            if (adjust === 'inner' || (adjust === 'normal' && isBorderBox))
1225            {
1226                fix += (parseFloat(style.borderLeftWidth) || 0) + (parseFloat(style.borderRightWidth) || 0);
1227            }
1228
1229            if (adjust === 'outer') fix -= (parseFloat(style.marginLeft) || 0) + (parseFloat(style.marginRight) || 0);
1230        }
1231
1232        return fix;
1233    },
1234    _getDim: function(type)
1235    {
1236        var node = this.get();
1237        return (node.nodeType === 3) ? { top: 0, left: 0 } : this['_get' + type](node);
1238    },
1239    _getPosition: function(node)
1240    {
1241        return { top: node.offsetTop, left: node.offsetLeft };
1242    },
1243    _getOffset: function(node)
1244    {
1245        var rect = node.getBoundingClientRect();
1246        var doc = node.ownerDocument;
1247		var docElem = doc.documentElement;
1248		var win = doc.defaultView;
1249
1250		return {
1251			top: rect.top + win.pageYOffset - docElem.clientTop,
1252			left: rect.left + win.pageXOffset - docElem.clientLeft
1253		};
1254    },
1255    _getSibling: function(selector, method)
1256    {
1257        selector = (selector && selector.sdom) ? selector.get() : selector;
1258
1259        var isNode = (selector && selector.nodeType);
1260        var sibling;
1261
1262        this.each(function(node)
1263        {
1264            while (node = node[method])
1265            {
1266                if ((isNode && node === selector) || new Dom(node).is(selector))
1267                {
1268                    sibling = node;
1269                    return;
1270                }
1271            }
1272        });
1273
1274        return new Dom(sibling);
1275    },
1276    _toArray: function(obj)
1277    {
1278        if (obj instanceof NodeList)
1279        {
1280            var arr = [];
1281            for (var i = 0; i < obj.length; i++)
1282            {
1283                arr[i] = obj[i];
1284            }
1285
1286            return arr;
1287        }
1288        else if (obj === undefined) return [];
1289        else
1290        {
1291            return (obj.sdom) ? obj.nodes : obj;
1292        }
1293    },
1294    _toParams: function(obj)
1295    {
1296        var params = '';
1297        for (var key in obj)
1298        {
1299            params += '&' + this._encodeUri(key) + '=' + this._encodeUri(obj[key]);
1300        }
1301
1302        return params.replace(/^&/, '');
1303    },
1304    _toObject: function(str)
1305    {
1306        return (new Function("return " + str))();
1307    },
1308    _encodeUri: function(str)
1309    {
1310        return encodeURIComponent(str).replace(/!/g, '%21').replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29').replace(/\*/g, '%2A').replace(/%20/g, '+');
1311    },
1312    _isNumber: function(str)
1313    {
1314        return !isNaN(str) && !isNaN(parseFloat(str));
1315    },
1316    _isObjectString: function(str)
1317    {
1318        return (str.search(/^{/) !== -1);
1319    },
1320    _getBooleanFromStr: function(str)
1321    {
1322        if (str === 'true') return true;
1323        else if (str === 'false') return false;
1324
1325        return str;
1326    },
1327    _hasDisplayNone: function(el)
1328    {
1329        return (el.style.display === 'none') || ((el.currentStyle) ? el.currentStyle.display : getComputedStyle(el, null).display) === 'none';
1330    }
1331};
1332// Unique ID
1333var uuid = 0;
1334
1335// Wrapper
1336var $R = function(selector, options)
1337{
1338    return RedactorApp(selector, options, [].slice.call(arguments, 2));
1339};
1340
1341// Globals
1342$R.app = [];
1343$R.version = '3.5.2';
1344$R.options = {};
1345$R.modules = {};
1346$R.services = {};
1347$R.classes = {};
1348$R.plugins = {};
1349$R.mixins = {};
1350$R.modals = {};
1351$R.lang = {};
1352$R.dom = function(selector, context) { return new Dom(selector, context); };
1353$R.ajax = Ajax;
1354$R.Dom = Dom;
1355$R.keycodes = {
1356	BACKSPACE: 8,
1357	DELETE: 46,
1358	UP: 38,
1359	DOWN: 40,
1360	ENTER: 13,
1361	SPACE: 32,
1362	ESC: 27,
1363	TAB: 9,
1364	CTRL: 17,
1365	META: 91,
1366	SHIFT: 16,
1367	ALT: 18,
1368	RIGHT: 39,
1369	LEFT: 37
1370};
1371$R.env = {
1372    'plugin': 'plugins',
1373    'module': 'modules',
1374    'service': 'services',
1375    'class': 'classes',
1376    'mixin': 'mixins'
1377};
1378
1379// jQuery Wrapper
1380/*eslint-env jquery*/
1381if (typeof jQuery !== 'undefined')
1382{
1383    (function($) { $.fn.redactor = function(options) { return RedactorApp(this.toArray(), options, [].slice.call(arguments, 1)); }; })(jQuery);
1384}
1385
1386// Class
1387var RedactorApp = function(selector, options, args)
1388{
1389    var namespace = 'redactor';
1390    var nodes = (Array.isArray(selector)) ? selector : (selector && selector.nodeType) ? [selector] : document.querySelectorAll(selector);
1391    var isApi = (typeof options === 'string' || typeof options === 'function');
1392    var value = [];
1393    var instance;
1394
1395    for (var i = 0; i < nodes.length; i++)
1396    {
1397        var el = nodes[i];
1398        var $el = $R.dom(el);
1399
1400        instance = $el.dataget(namespace);
1401        if (!instance && !isApi)
1402        {
1403            // Initialization
1404            instance = new App(el, options, uuid);
1405            $el.dataset(namespace, instance);
1406            $R.app[uuid] = instance;
1407            uuid++;
1408        }
1409
1410        // API
1411        if (instance && isApi)
1412        {
1413            var isDestroy = (options === 'destroy');
1414            options = (isDestroy) ? 'stop' : options;
1415
1416            var methodValue;
1417            if (typeof options === 'function')
1418            {
1419                methodValue = options.apply(instance, args);
1420            }
1421            else
1422            {
1423                args.unshift(options);
1424                methodValue = instance.api.apply(instance, args);
1425            }
1426            if (methodValue !== undefined) value.push(methodValue);
1427
1428            if (isDestroy) $el.dataset(namespace, false);
1429        }
1430    }
1431
1432    return (value.length === 0 || value.length === 1) ? ((value.length === 0) ? instance : value[0]) : value;
1433};
1434
1435// add
1436$R.add = function(type, name, obj)
1437{
1438    if (typeof $R.env[type] === 'undefined') return;
1439
1440    // translations
1441    if (obj.translations)
1442    {
1443        $R.lang = $R.extend(true, {}, $R.lang, obj.translations);
1444    }
1445
1446    // modals
1447    if (obj.modals)
1448    {
1449        $R.modals = $R.extend(true, {}, $R.modals, obj.modals);
1450    }
1451
1452    // mixin
1453    if (type === 'mixin')
1454    {
1455        $R[$R.env[type]][name] = obj;
1456    }
1457    else
1458    {
1459        // prototype
1460        var F = function() {};
1461        F.prototype = obj;
1462
1463        // mixins
1464        if (obj.mixins)
1465        {
1466            for (var i = 0; i < obj.mixins.length; i++)
1467            {
1468                $R.inherit(F, $R.mixins[obj.mixins[i]]);
1469            }
1470        }
1471
1472        $R[$R.env[type]][name] = F;
1473    }
1474};
1475
1476// add lang
1477$R.addLang = function(lang, obj)
1478{
1479    if (typeof $R.lang[lang] === 'undefined')
1480    {
1481        $R.lang[lang] = {};
1482    }
1483
1484    $R.lang[lang] = $R.extend($R.lang[lang], obj);
1485};
1486
1487// create
1488$R.create = function(name)
1489{
1490    var arr = name.split('.');
1491    var args = [].slice.call(arguments, 1);
1492
1493    var type = 'classes';
1494    if (typeof $R.env[arr[0]] !== 'undefined')
1495    {
1496        type = $R.env[arr[0]];
1497        name = arr.slice(1).join('.');
1498    }
1499
1500    // construct
1501    var instance = new $R[type][name]();
1502
1503    // init
1504    if (instance.init)
1505    {
1506        var res = instance.init.apply(instance, args);
1507
1508        return (res) ? res : instance;
1509    }
1510
1511    return instance;
1512};
1513
1514// inherit
1515$R.inherit = function(current, parent)
1516{
1517    var F = function () {};
1518    F.prototype = parent;
1519    var f = new F();
1520
1521    for (var prop in current.prototype)
1522    {
1523        if (current.prototype.__lookupGetter__(prop)) f.__defineGetter__(prop, current.prototype.__lookupGetter__(prop));
1524        else f[prop] = current.prototype[prop];
1525    }
1526
1527    current.prototype = f;
1528    current.prototype.super = parent;
1529
1530    return current;
1531};
1532
1533// error
1534$R.error = function(exception)
1535{
1536    throw exception;
1537};
1538
1539// extend
1540$R.extend = function()
1541{
1542    var extended = {};
1543    var deep = false;
1544    var i = 0;
1545    var length = arguments.length;
1546
1547    if (Object.prototype.toString.call( arguments[0] ) === '[object Boolean]')
1548    {
1549        deep = arguments[0];
1550        i++;
1551    }
1552
1553    var merge = function(obj)
1554    {
1555        for (var prop in obj)
1556        {
1557            if (Object.prototype.hasOwnProperty.call(obj, prop))
1558            {
1559                if (deep && Object.prototype.toString.call(obj[prop]) === '[object Object]') extended[prop] = $R.extend(true, extended[prop], obj[prop]);
1560                else extended[prop] = obj[prop];
1561            }
1562        }
1563    };
1564
1565    for (; i < length; i++ )
1566    {
1567        var obj = arguments[i];
1568        merge(obj);
1569    }
1570
1571    return extended;
1572};
1573$R.opts = {
1574    animation: true,
1575    lang: 'en',
1576    direction: 'ltr',
1577    spellcheck: true,
1578    structure: false,
1579    scrollTarget: false,
1580    styles: true,
1581    stylesClass: 'redactor-styles',
1582    placeholder: false,
1583
1584    source: true,
1585    showSource: false,
1586
1587    inline: false,
1588
1589    breakline: false,
1590    markup: 'p',
1591    enterKey: true,
1592
1593    clickToEdit: false,
1594    clickToSave: false,
1595    clickToCancel: false,
1596
1597    focus: false,
1598    focusEnd: false,
1599
1600    minHeight: false, // string, '100px'
1601    maxHeight: false, // string, '100px'
1602    maxWidth: false, // string, '700px'
1603
1604    plugins: [], // array
1605    callbacks: {},
1606
1607    // pre & tab
1608    preClass: false, // string
1609    preSpaces: 4, // or false
1610    tabindex: false, // int
1611    tabAsSpaces: false, // true or number of spaces
1612    tabKey: true,
1613
1614    // autosave
1615    autosave: false, // false or url
1616    autosaveName: false,
1617    autosaveData: false,
1618    autosaveMethod: 'post',
1619
1620    // toolbar
1621    toolbar: true,
1622    toolbarFixed: true,
1623    toolbarFixedTarget: document,
1624    toolbarFixedTopOffset: 0, // pixels
1625    toolbarExternal: false, // ID selector
1626    toolbarContext: true,
1627
1628    // air
1629    air: false,
1630
1631    // formatting
1632    formatting: ['p', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
1633    formattingAdd: false,
1634    formattingHide: false,
1635
1636    // buttons
1637    buttons: ['html', 'format', 'bold', 'italic', 'deleted', 'lists', 'image', 'file', 'link'],
1638    // + 'line', 'redo', 'undo', 'underline', 'ol', 'ul', 'indent', 'outdent'
1639    buttonsTextLabeled: false,
1640    buttonsAdd: [],
1641    buttonsAddFirst: [],
1642    buttonsAddAfter: false,
1643    buttonsAddBefore: false,
1644    buttonsHide: [],
1645    buttonsHideOnMobile: [],
1646
1647    // image
1648    imageUpload: false,
1649    imageUploadParam: 'file',
1650    imageData: false,
1651    imageEditable: true,
1652    imageCaption: true,
1653    imageLink: true,
1654    imagePosition: false,
1655    imageResizable: false,
1656    imageFloatMargin: '10px',
1657    imageFigure: true,
1658    imageObserve: true,
1659    imageSrcData: false,
1660
1661    // file
1662    fileUpload: false,
1663    fileUploadParam: 'file',
1664    fileData: false,
1665    fileAttachment: false,
1666
1667    // upload opts
1668    uploadData: false,
1669    dragUpload: true,
1670    multipleUpload: true,
1671    clipboardUpload: true,
1672    uploadBase64: false,
1673
1674    // link
1675    linkTarget: false,
1676    linkTitle: false,
1677    linkNewTab: true,
1678    linkNofollow: false,
1679    linkSize: 30,
1680    linkValidation: true,
1681
1682    // clean
1683    cleanOnEnter: true,
1684    cleanInlineOnEnter: false,
1685    paragraphize: true,
1686    removeScript: true,
1687    removeNewLines: false,
1688    removeComments: true,
1689    replaceTags: {
1690        'b': 'strong',
1691        'i': 'em',
1692        'strike': 'del'
1693    },
1694
1695    // paste
1696    pastePlainText: false,
1697    pasteLinkTarget: false,
1698    pasteImages: true,
1699    pasteLinks: true,
1700    pasteClean: true,
1701    pasteKeepStyle: [],
1702    pasteKeepClass: [],
1703    pasteKeepAttrs: ['td', 'th'],
1704    pasteBlockTags: ['pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'table', 'tbody', 'thead', 'tfoot', 'th', 'tr', 'td', 'ul', 'ol', 'li', 'blockquote', 'p', 'figure', 'figcaption'],
1705    pasteInlineTags: ['a', 'img', 'br', 'strong', 'ins', 'code', 'del', 'span', 'samp', 'kbd', 'sup', 'sub', 'mark', 'var', 'cite', 'small', 'b', 'u', 'em', 'i', 'abbr'],
1706
1707    // active buttons
1708    activeButtons: {
1709        b: 'bold',
1710        strong: 'bold',
1711        i: 'italic',
1712        em: 'italic',
1713        del: 'deleted',
1714        strike: 'deleted',
1715        u: 'underline'
1716    },
1717    activeButtonsAdd: {},
1718    activeButtonsObservers: {},
1719
1720    // autoparser
1721    autoparse: true,
1722    autoparseStart: true,
1723    autoparsePaste: true,
1724    autoparseLinks: true,
1725    autoparseImages: true,
1726    autoparseVideo: true,
1727    autoparseHttps: false,
1728
1729    // shortcodes
1730    shortcodes: {
1731        'p.': { format: 'p' },
1732        'quote.': { format: 'blockquote' },
1733        'pre.': { format: 'pre' },
1734        'h1.': { format: 'h1' },
1735        'h2.': { format: 'h2' },
1736        'h3.': { format: 'h3' },
1737        'h4.': { format: 'h4' },
1738        'h5.': { format: 'h5' },
1739        'h6.': { format: 'h6' },
1740        //'1.': { format: 'ol' },
1741        '*.': { format: 'ul' }
1742    },
1743    shortcodesAdd: false, // object
1744
1745    // shortcuts
1746    shortcuts: {
1747        'ctrl+shift+m, meta+shift+m': { api: 'module.inline.clearformat' },
1748        'ctrl+b, meta+b': { api: 'module.inline.format', args: 'b' },
1749        'ctrl+i, meta+i': { api: 'module.inline.format', args: 'i' },
1750        'ctrl+u, meta+u': { api: 'module.inline.format', args: 'u' },
1751        'ctrl+h, meta+h': { api: 'module.inline.format', args: 'sup' },
1752        'ctrl+l, meta+l': { api: 'module.inline.format', args: 'sub' },
1753        'ctrl+k, meta+k': { api: 'module.link.open' },
1754        'ctrl+alt+0, meta+alt+0': { api: 'module.block.format', args: 'p' },
1755        'ctrl+alt+1, meta+alt+1': { api: 'module.block.format', args: 'h1' },
1756        'ctrl+alt+2, meta+alt+2': { api: 'module.block.format', args: 'h2' },
1757        'ctrl+alt+3, meta+alt+3': { api: 'module.block.format', args: 'h3' },
1758        'ctrl+alt+4, meta+alt+4': { api: 'module.block.format', args: 'h4' },
1759        'ctrl+alt+5, meta+alt+5': { api: 'module.block.format', args: 'h5' },
1760        'ctrl+alt+6, meta+alt+6': { api: 'module.block.format', args: 'h6' },
1761        'ctrl+shift+7, meta+shift+7': { api: 'module.list.toggle', args: 'ol' },
1762        'ctrl+shift+8, meta+shift+8': { api: 'module.list.toggle', args: 'ul' }
1763    },
1764    shortcutsAdd: false, // object
1765
1766    // misc
1767    grammarly: true,
1768    notranslate: false,
1769
1770    // private
1771    bufferLimit: 100,
1772    emptyHtml: '<p></p>',
1773    markerChar: '\ufeff',
1774    imageTypes: ['image/png', 'image/jpeg', 'image/gif'],
1775    imageAttrs: ['alt', 'title', 'src', 'class', 'width', 'height', 'srcset'],
1776    inlineTags: ['a', 'span', 'strong', 'strike', 'b', 'u', 'em', 'i', 'code', 'del', 'ins', 'samp', 'kbd', 'sup', 'sub', 'mark', 'var', 'cite', 'small', 'abbr'],
1777    blockTags: ['pre', 'ul', 'ol', 'li', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',  'dl', 'dt', 'dd', 'div', 'table', 'tbody', 'thead', 'tfoot', 'tr', 'th', 'td', 'blockquote', 'output', 'figcaption', 'figure', 'address', 'section', 'header', 'footer', 'aside', 'article', 'iframe'],
1778    regex: {
1779        youtube: /^https?\:\/\/(?:www\.youtube(?:\-nocookie)?\.com\/|m\.youtube\.com\/|youtube\.com\/)?(?:ytscreeningroom\?vi?=|youtu\.be\/|vi?\/|user\/.+\/u\/\w{1,2}\/|embed\/|watch\?(?:.*\&)?vi?=|\&vi?=|\?(?:.*\&)?vi?=)([^#\&\?\n\/<>"']*)/gi,
1780        vimeo: /(http|https)?:\/\/(?:www.|player.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^/]*)\/videos\/|album\/(?:\d+)\/video\/|video\/|)(\d+)(?:\/[a-zA-Z0-9_-]+)?/gi,
1781        imageurl: /((https?|www)[^\s]+\.)(jpe?g|png|gif)(\?[^\s-]+)?/gi,
1782        url: /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z\u00F0-\u02AF0-9()!@:%_+.~#?&//=]*)/gi,
1783        aurl1: /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim,
1784        aurl2: /(^|[^\/])(www\.[\S]+(\b|$))/gim
1785    },
1786    input: true,
1787    zindex: false,
1788    modes: {
1789        "inline": {
1790            pastePlainText: true,
1791            pasteImages: false,
1792            enterKey: false,
1793            toolbar: false,
1794            autoparse: false,
1795            source: false,
1796            showSource: false,
1797            styles: false,
1798            air: false
1799        },
1800        "original": {
1801            styles: false
1802        }
1803    }
1804};
1805$R.lang['en'] = {
1806    "format": "Format",
1807    "image": "Image",
1808    "file": "File",
1809    "link": "Link",
1810    "bold": "Bold",
1811    "italic": "Italic",
1812    "deleted": "Strikethrough",
1813    "underline": "Underline",
1814    "superscript": "Superscript",
1815    "subscript": "Subscript",
1816    "bold-abbr": "B",
1817    "italic-abbr": "I",
1818    "deleted-abbr": "S",
1819    "underline-abbr": "U",
1820    "superscript-abbr": "Sup",
1821    "subscript-abbr": "Sub",
1822    "lists": "Lists",
1823    "link-insert": "Insert Link",
1824    "link-edit": "Edit Link",
1825    "link-in-new-tab": "Open link in new tab",
1826    "unlink": "Unlink",
1827    "cancel": "Cancel",
1828    "close": "Close",
1829    "insert": "Insert",
1830    "save": "Save",
1831    "delete": "Delete",
1832    "text": "Text",
1833    "edit": "Edit",
1834    "title": "Alt",
1835    "paragraph": "Normal text",
1836    "quote": "Quote",
1837    "code": "Code",
1838    "heading1": "Heading 1",
1839    "heading2": "Heading 2",
1840    "heading3": "Heading 3",
1841    "heading4": "Heading 4",
1842    "heading5": "Heading 5",
1843    "heading6": "Heading 6",
1844    "filename": "Name",
1845    "optional": "optional",
1846    "unorderedlist": "Unordered List",
1847    "orderedlist": "Ordered List",
1848    "outdent": "Outdent",
1849    "indent": "Indent",
1850    "horizontalrule": "Line",
1851    "upload": "Upload",
1852    "upload-label": "Drop files here or click to upload",
1853    "accessibility-help-label": "Rich text editor",
1854    "caption": "Caption",
1855    "bulletslist": "Bullets",
1856    "numberslist": "Numbers",
1857    "image-position": "Position",
1858    "none": "None",
1859    "left": "Left",
1860    "right": "Right",
1861    "center": "Center",
1862    "undo": "Undo",
1863    "redo": "Redo"
1864};
1865$R.buttons = {
1866    html: {
1867        title: 'HTML',
1868        icon: true,
1869        api: 'module.source.toggle'
1870    },
1871    undo: {
1872        title: '## undo ##',
1873        icon: true,
1874        api: 'module.buffer.undo'
1875    },
1876    redo: {
1877        title: '## redo ##',
1878        icon: true,
1879        api: 'module.buffer.redo'
1880    },
1881    format: {
1882        title: '## format ##',
1883        icon: true,
1884        dropdown: {
1885            p: {
1886                title: '## paragraph ##',
1887                api: 'module.block.format',
1888                args: {
1889                    tag: 'p'
1890                }
1891            },
1892            blockquote: {
1893                title: '## quote ##',
1894                api: 'module.block.format',
1895                args: {
1896                    tag: 'blockquote'
1897                }
1898            },
1899            pre: {
1900                title: '## code ##',
1901                api: 'module.block.format',
1902                args: {
1903                    tag: 'pre'
1904                }
1905            },
1906            h1: {
1907                title: '## heading1 ##',
1908                api: 'module.block.format',
1909                args: {
1910                    tag: 'h1'
1911                }
1912            },
1913            h2: {
1914                title: '## heading2 ##',
1915                api: 'module.block.format',
1916                args: {
1917                    tag: 'h2'
1918                }
1919            },
1920            h3: {
1921                title: '## heading3 ##',
1922                api: 'module.block.format',
1923                args: {
1924                    tag: 'h3'
1925                }
1926            },
1927            h4: {
1928                title: '## heading4 ##',
1929                api: 'module.block.format',
1930                args: {
1931                    tag: 'h4'
1932                }
1933            },
1934            h5: {
1935                title: '## heading5 ##',
1936                api: 'module.block.format',
1937                args: {
1938                    tag: 'h5'
1939                }
1940            },
1941            h6: {
1942                title: '## heading6 ##',
1943                api: 'module.block.format',
1944                args: {
1945                    tag: 'h6'
1946                }
1947            }
1948        }
1949    },
1950    bold: {
1951        title: '## bold-abbr ##',
1952        icon: true,
1953        tooltip: '## bold ##',
1954        api: 'module.inline.format',
1955        args: {
1956            tag: 'b'
1957        }
1958    },
1959    italic: {
1960        title: '## italic-abbr ##',
1961        icon: true,
1962        tooltip: '## italic ##',
1963        api: 'module.inline.format',
1964        args: {
1965            tag: 'i'
1966        }
1967    },
1968    deleted: {
1969        title: '## deleted-abbr ##',
1970        icon: true,
1971        tooltip: '## deleted ##',
1972        api: 'module.inline.format',
1973        args: {
1974            tag: 'del'
1975        }
1976    },
1977    underline: {
1978        title: '## underline-abbr ##',
1979        icon: true,
1980        tooltip: '## underline ##',
1981        api: 'module.inline.format',
1982        args: {
1983            tag: 'u'
1984        }
1985    },
1986    sup: {
1987        title: '## superscript-abbr ##',
1988        icon: true,
1989        tooltip: '## superscript ##',
1990        api: 'module.inline.format',
1991        args: {
1992            tag: 'sup'
1993        }
1994    },
1995    sub: {
1996        title: '## subscript-abbr ##',
1997        icon: true,
1998        tooltip: '## subscript ##',
1999        api: 'module.inline.format',
2000        args: {
2001            tag: 'sub'
2002        }
2003    },
2004    lists: {
2005        title: '## lists ##',
2006        icon: true,
2007        observe: 'list',
2008        dropdown: {
2009            observe: 'list',
2010            unorderedlist: {
2011                title: '&bull; ## unorderedlist ##',
2012                api: 'module.list.toggle',
2013                args: 'ul'
2014            },
2015            orderedlist: {
2016                title: '1. ## orderedlist ##',
2017                api: 'module.list.toggle',
2018                args: 'ol'
2019            },
2020            outdent: {
2021                title: '< ## outdent ##',
2022                api: 'module.list.outdent'
2023            },
2024            indent: {
2025                title: '> ## indent ##',
2026                api: 'module.list.indent'
2027            }
2028        }
2029    },
2030    ul: {
2031        title: '&bull; ## bulletslist ##',
2032        icon: true,
2033        api: 'module.list.toggle',
2034        observe: 'list',
2035        args: 'ul'
2036    },
2037    ol: {
2038        title: '1. ## numberslist ##',
2039        icon: true,
2040        api: 'module.list.toggle',
2041        observe: 'list',
2042        args: 'ol'
2043    },
2044    outdent: {
2045        title: '## outdent ##',
2046        icon: true,
2047        api: 'module.list.outdent',
2048        observe: 'list'
2049    },
2050    indent: {
2051        title: '## indent ##',
2052        icon: true,
2053        api: 'module.list.indent',
2054        observe: 'list'
2055    },
2056    image: {
2057        title: '## image ##',
2058        icon: true,
2059        api: 'module.image.open'
2060    },
2061    file: {
2062        title: '## file ##',
2063        icon: true,
2064        api: 'module.file.open'
2065    },
2066    link: {
2067        title: '## link ##',
2068        icon: true,
2069        observe: 'link',
2070        dropdown: {
2071            observe: 'link',
2072            link: {
2073                title: '## link-insert ##',
2074                api: 'module.link.open'
2075            },
2076            unlink: {
2077                title: '## unlink ##',
2078                api: 'module.link.unlink'
2079            }
2080        }
2081    },
2082    line: {
2083        title: '## horizontalrule ##',
2084        icon: true,
2085        api: 'module.line.insert'
2086    }
2087};
2088var App = function(element, options, uuid)
2089{
2090    this.module = {};
2091    this.plugin = {};
2092    this.instances = {};
2093
2094    // start/stop
2095    this.started = false;
2096    this.stopped = false;
2097
2098    // environment
2099    this.uuid = uuid;
2100    this.rootElement = element;
2101    this.rootOpts = options;
2102    this.dragInside = false;
2103    this.dragComponentInside = false;
2104    this.keycodes = $R.keycodes;
2105    this.namespace = 'redactor';
2106    this.$win = $R.dom(window);
2107    this.$doc = $R.dom(document);
2108    this.$body = $R.dom('body');
2109    this.editorReadOnly = false;
2110
2111    // core services
2112    this.opts = $R.create('service.options', options, element);
2113    this.lang = $R.create('service.lang', this);
2114
2115    // build
2116    this.buildServices();
2117    this.buildModules();
2118    this.buildPlugins();
2119
2120    // start
2121    this.start();
2122};
2123
2124App.prototype = {
2125    start: function()
2126    {
2127        // start
2128        this.stopped = false;
2129        this.broadcast('start');
2130        this.broadcast('startcode');
2131
2132        if (this.opts.clickToEdit)
2133        {
2134            this.broadcast('startclicktoedit');
2135        }
2136        else
2137        {
2138            this.broadcast('enable');
2139            if (this.opts.showSource) this.broadcast('startcodeshow');
2140            this.broadcast('enablefocus');
2141        }
2142
2143        // started
2144        this.broadcast('started');
2145        this.started = true;
2146    },
2147    stop: function()
2148    {
2149        this.started = false;
2150        this.stopped = true;
2151
2152        this.broadcast('stop');
2153        this.broadcast('disable');
2154        this.broadcast('stopped');
2155    },
2156
2157    // started & stopped
2158    isStarted: function()
2159    {
2160        return this.started;
2161    },
2162    isStopped: function()
2163    {
2164        return this.stopped;
2165    },
2166
2167    // build
2168    buildServices: function()
2169    {
2170        var core = ['options', 'lang'];
2171        var bindable = ['uuid', 'keycodes', 'opts', 'lang', '$win', '$doc', '$body'];
2172        var services = [];
2173        for (var name in $R.services)
2174        {
2175            if (core.indexOf(name) === -1)
2176            {
2177                this[name] = $R.create('service.' + name, this);
2178                services.push(name);
2179                bindable.push(name);
2180            }
2181        }
2182
2183        // binding
2184        for (var i = 0; i < services.length; i++)
2185        {
2186            var service = services[i];
2187            for (var z = 0; z < bindable.length; z++)
2188            {
2189                var inj = bindable[z];
2190                if (service !== inj)
2191                {
2192                    this[service][inj] = this[inj];
2193                }
2194            }
2195        }
2196    },
2197    buildModules: function()
2198    {
2199        for (var name in $R.modules)
2200        {
2201            this.module[name] = $R.create('module.' + name, this);
2202            this.instances[name] = this.module[name];
2203        }
2204    },
2205    buildPlugins: function()
2206    {
2207        var plugins = this.opts.plugins;
2208        for (var i = 0; i < plugins.length; i++)
2209        {
2210            var name = plugins[i];
2211            if (typeof $R.plugins[name] !== 'undefined')
2212            {
2213                this.plugin[name] = $R.create('plugin.' + name, this);
2214                this.instances[name] = this.plugin[name];
2215            }
2216        }
2217    },
2218
2219    // draginside
2220    isDragInside: function()
2221    {
2222        return this.dragInside;
2223    },
2224    setDragInside: function(dragInside)
2225    {
2226        this.dragInside = dragInside;
2227    },
2228    isDragComponentInside: function()
2229    {
2230        return this.dragComponentInside;
2231    },
2232    setDragComponentInside: function(dragInside)
2233    {
2234        this.dragComponentInside = dragInside;
2235    },
2236    getDragComponentInside: function()
2237    {
2238        return this.dragComponentInside;
2239    },
2240
2241    // readonly
2242    isReadOnly: function()
2243    {
2244        return this.editorReadOnly;
2245    },
2246    enableReadOnly: function()
2247    {
2248        this.editorReadOnly = true;
2249        this.broadcast('enablereadonly');
2250        this.component.clearActive();
2251        this.toolbar.disableButtons();
2252    },
2253    disableReadOnly: function()
2254    {
2255        this.editorReadOnly = false;
2256        this.broadcast('disablereadonly');
2257        this.toolbar.enableButtons();
2258    },
2259
2260    // messaging
2261    callMessageHandler: function(instance, name, args)
2262    {
2263        var arr = name.split('.');
2264        var value;
2265        if (arr.length === 1)
2266        {
2267            if (typeof instance['on' + name] === 'function')
2268            {
2269                value = instance['on' + name].apply(instance, args);
2270            }
2271        }
2272        else
2273        {
2274            arr[0] = 'on' + arr[0];
2275
2276            var func = this.utils.checkProperty(instance, arr);
2277            if (typeof func === 'function')
2278            {
2279                value = func.apply(instance, args);
2280            }
2281        }
2282
2283        return value;
2284    },
2285    broadcast: function(name)
2286    {
2287        var args = [].slice.call(arguments, 1);
2288        var returned;
2289        for (var moduleName in this.instances)
2290        {
2291            var value = this.callMessageHandler(this.instances[moduleName], name, args);
2292            if (typeof value !== 'undefined') {
2293                returned = value;
2294            }
2295        }
2296
2297        // callback
2298        var cval = this.callback.trigger(name, args);
2299
2300        return (typeof returned !== 'undefined') ? returned : cval;
2301    },
2302
2303    // callback
2304    on: function(name, func)
2305    {
2306        this.callback.add(name, func);
2307    },
2308    off: function(name, func)
2309    {
2310        this.callback.remove(name, func);
2311    },
2312
2313    // api
2314    api: function(name)
2315    {
2316        if (!this.isStarted() && name !== 'start') return;
2317        if (this.isReadOnly() && name !== 'disableReadOnly') return;
2318
2319        this.broadcast('state');
2320
2321        var args = [].slice.call(arguments, 1);
2322        var arr = name.split('.');
2323
2324        var isApp = (arr.length === 1);
2325        var isCallback = (arr[0] === 'on' || arr[0] === 'off');
2326        var isService = (!isCallback && arr.length === 2);
2327        var isPlugin = (arr[0] === 'plugin');
2328        var isModule = (arr[0] === 'module');
2329
2330        // app
2331        if (isApp)
2332        {
2333            if (typeof this[arr[0]] === 'function')
2334            {
2335                return this.callInstanceMethod(this, arr[0], args);
2336            }
2337        }
2338        // callback
2339        else if (isCallback)
2340        {
2341            return (arr[0] === 'on') ? this.on(arr[1], args[0]) : this.off(arr[1], args[0] || undefined);
2342        }
2343        // service
2344        else if (isService)
2345        {
2346            if (this.isInstanceExists(this, arr[0]))
2347            {
2348                return this.callInstanceMethod(this[arr[0]], arr[1], args);
2349            }
2350            else
2351            {
2352                $R.error(new Error('Service "' + arr[0] + '" not found'));
2353            }
2354        }
2355        // plugin
2356        else if (isPlugin)
2357        {
2358            if (this.isInstanceExists(this.plugin, arr[1]))
2359            {
2360                return this.callInstanceMethod(this.plugin[arr[1]], arr[2], args);
2361            }
2362            else
2363            {
2364                $R.error(new Error('Plugin "' + arr[1] + '" not found'));
2365            }
2366        }
2367        // module
2368        else if (isModule)
2369        {
2370            if (this.isInstanceExists(this.module, arr[1]))
2371            {
2372                return this.callInstanceMethod(this.module[arr[1]], arr[2], args);
2373            }
2374            else
2375            {
2376                $R.error(new Error('Module "' + arr[1] + '" not found'));
2377            }
2378        }
2379
2380    },
2381    isInstanceExists: function(obj, name)
2382    {
2383        return (typeof obj[name] !== 'undefined');
2384    },
2385    callInstanceMethod: function(instance, method, args)
2386    {
2387        if (typeof instance[method] === 'function')
2388        {
2389            return instance[method].apply(instance, args);
2390        }
2391    }
2392};
2393$R.add('mixin', 'formatter', {
2394
2395    // public
2396    buildArgs: function(args)
2397    {
2398        this.args = {
2399            'class': args['class'] || false,
2400            'style': args['style'] || false,
2401            'attr': args['attr'] || false
2402        };
2403
2404        if (!this.args['class'] && !this.args['style'] && !this.args['attr'])
2405        {
2406            this.args = false;
2407        }
2408    },
2409    applyArgs: function(nodes, selection)
2410    {
2411        if (this.args)
2412        {
2413            nodes = this[this.type](this.args, false, nodes, selection);
2414        }
2415        else
2416        {
2417            nodes = this._clearAll(nodes, selection);
2418        }
2419
2420        return nodes;
2421    },
2422    clearClass: function(tags, nodes)
2423    {
2424        this.selection.save();
2425
2426        var $elements = (nodes) ? $R.dom(nodes) : this.getElements(tags, true);
2427        $elements.removeAttr('class');
2428
2429        nodes = this._unwrapSpanWithoutAttr($elements.getAll());
2430
2431        this.selection.restore();
2432
2433        return nodes;
2434    },
2435    clearStyle: function(tags, nodes)
2436    {
2437        this.selection.save();
2438
2439        var $elements = (nodes) ? $R.dom(nodes) : this.getElements(tags, true);
2440        $elements.removeAttr('style');
2441
2442        nodes = this._unwrapSpanWithoutAttr($elements.getAll());
2443
2444        this.selection.restore();
2445
2446        return nodes;
2447    },
2448    clearAttr: function(tags, nodes)
2449    {
2450        this.selection.save();
2451
2452        var $elements = (nodes) ? $R.dom(nodes) : this.getElements(tags, true);
2453        this._removeAllAttr($elements);
2454
2455        nodes = this._unwrapSpanWithoutAttr($elements.getAll());
2456
2457        this.selection.restore();
2458
2459        return nodes;
2460    },
2461    set: function(args, tags, nodes, selection)
2462    {
2463        if (selection !== false) this.selection.save();
2464
2465        var $elements = (nodes) ? $R.dom(nodes) : this.getElements(tags);
2466
2467        if (args['class'])
2468        {
2469            $elements.removeAttr('class');
2470            $elements.addClass(args['class']);
2471        }
2472
2473        if (args['style'])
2474        {
2475            $elements.removeAttr('style');
2476            $elements.css(args['style']);
2477            $elements.each(function(node)
2478            {
2479                var $node = $R.dom(node);
2480                $node.attr('data-redactor-style-cache', $node.attr('style'));
2481            });
2482        }
2483
2484        if (args['attr'])
2485        {
2486            this._removeAllAttr($elements);
2487            $elements.attr(args['attr']);
2488        }
2489
2490        if (selection !== false) this.selection.restore();
2491
2492        return $elements.getAll();
2493    },
2494    toggle: function(args, tags, nodes, selection)
2495    {
2496        if (selection !== false) this.selection.save();
2497
2498        var $elements = (nodes) ? $R.dom(nodes) : this.getElements(tags);
2499
2500        if (args['class'])
2501        {
2502            $elements.toggleClass(args['class']);
2503            $elements.each(function(node)
2504            {
2505                if (node.className === '') node.removeAttribute('class');
2506            });
2507        }
2508
2509        var params;
2510        if (args['style'])
2511        {
2512            params = args['style'];
2513            $elements.each(function(node)
2514            {
2515                var $node = $R.dom(node);
2516                for (var key in params)
2517                {
2518                    var newVal = params[key];
2519                    var oldVal = $node.css(key);
2520
2521                    oldVal = (this.utils.isRgb(oldVal)) ? this.utils.rgb2hex(oldVal) : oldVal.replace(/"/g, '');
2522                    newVal = (this.utils.isRgb(newVal)) ? this.utils.rgb2hex(newVal) : newVal.replace(/"/g, '');
2523
2524                    oldVal = this.utils.hex2long(oldVal);
2525                    newVal = this.utils.hex2long(newVal);
2526
2527                    var compareNew = (typeof newVal === 'string') ? newVal.toLowerCase() : newVal;
2528                    var compareOld = (typeof oldVal === 'string') ? oldVal.toLowerCase() : oldVal;
2529
2530                    if (compareNew === compareOld) $node.css(key, '');
2531                    else $node.css(key, newVal);
2532                }
2533
2534                this._convertStyleQuotes($node);
2535                if (this.utils.removeEmptyAttr(node, 'style'))
2536                {
2537                    $node.removeAttr('data-redactor-style-cache');
2538                }
2539                else
2540                {
2541                    $node.attr('data-redactor-style-cache', $node.attr('style'));
2542                }
2543
2544            }.bind(this));
2545        }
2546
2547        if (args['attr'])
2548        {
2549            params = args['attr'];
2550            $elements.each(function(node)
2551            {
2552                var $node = $R.dom(node);
2553                for (var key in params)
2554                {
2555                    if ($node.attr(key)) $node.removeAttr(key);
2556                    else $node.attr(key, params[key]);
2557                }
2558            });
2559
2560        }
2561
2562        if (selection !== false) this.selection.restore();
2563
2564        return $elements.getAll();
2565    },
2566    add: function(args, tags, nodes, selection)
2567    {
2568        if (selection !== false) this.selection.save();
2569
2570        var $elements = (nodes) ? $R.dom(nodes) : this.getElements(tags);
2571
2572        if (args['class'])
2573        {
2574            $elements.addClass(args['class']);
2575        }
2576
2577        if (args['style'])
2578        {
2579            var params = args['style'];
2580            $elements.each(function(node)
2581            {
2582                var $node = $R.dom(node);
2583                $node.css(params);
2584                $node.attr('data-redactor-style-cache', $node.attr('style'));
2585
2586                this._convertStyleQuotes($node);
2587
2588            }.bind(this));
2589        }
2590
2591        if (args['attr'])
2592        {
2593            $elements.attr(args['attr']);
2594        }
2595
2596        if (selection !== false) this.selection.restore();
2597
2598        return $elements.getAll();
2599    },
2600    remove: function(args, tags, nodes, selection)
2601    {
2602        if (selection !== false) this.selection.save();
2603
2604        var $elements = (nodes) ? $R.dom(nodes) : this.getElements(tags);
2605
2606        if (args['class'])
2607        {
2608            $elements.removeClass(args['class']);
2609            $elements.each(function(node)
2610            {
2611                if (node.className === '') node.removeAttribute('class');
2612            });
2613        }
2614
2615        if (args['style'])
2616        {
2617            var name = args['style'];
2618            $elements.each(function(node)
2619            {
2620                var $node = $R.dom(node);
2621                $node.css(name, '');
2622
2623                if (this.utils.removeEmptyAttr(node, 'style'))
2624                {
2625                    $node.removeAttr('data-redactor-style-cache');
2626                }
2627                else
2628                {
2629                    $node.attr('data-redactor-style-cache', $node.attr('style'));
2630                }
2631
2632            }.bind(this));
2633        }
2634
2635        if (args['attr'])
2636        {
2637            $elements.removeAttr(args['attr']);
2638        }
2639
2640        nodes = this._unwrapSpanWithoutAttr($elements.getAll());
2641
2642        if (selection !== false) this.selection.restore();
2643
2644        return nodes;
2645    },
2646
2647    // private
2648    _removeAllAttr: function($elements)
2649    {
2650        $elements.each(function(node)
2651        {
2652            for (var i = node.attributes.length; i-->0;)
2653            {
2654                var nodeAttr = node.attributes[i];
2655                var name = nodeAttr.name;
2656                if (name !== 'style' && name !== 'class')
2657                {
2658                    node.removeAttributeNode(nodeAttr);
2659                }
2660            }
2661        });
2662    },
2663    _convertStyleQuotes: function($node)
2664    {
2665        var style = $node.attr('style');
2666        if (style) $node.attr('style', style.replace(/"/g, '\''));
2667    },
2668    _clearAll: function(nodes, selection)
2669    {
2670        if (selection !== false) this.selection.save();
2671
2672        for (var i = 0; i < nodes.length; i++)
2673        {
2674            var node = nodes[i];
2675            while (node.attributes.length > 0)
2676            {
2677                node.removeAttribute(node.attributes[0].name);
2678            }
2679        }
2680
2681        nodes = this._unwrapSpanWithoutAttr(nodes);
2682
2683        if (selection !== false) this.selection.restore();
2684
2685        return nodes;
2686    },
2687    _unwrapSpanWithoutAttr: function(nodes)
2688    {
2689        var finalNodes = [];
2690        for (var i = 0; i < nodes.length; i++)
2691        {
2692            var node = nodes[i];
2693            var len = node.attributes.length;
2694            if (len <= 0 && node.nodeType !== 3 && node.tagName === 'SPAN')
2695            {
2696                $R.dom(node).unwrap();
2697            }
2698            else
2699            {
2700                finalNodes.push(node);
2701            }
2702        }
2703
2704        return finalNodes;
2705    }
2706});
2707$R.add('mixin', 'dom', $R.Dom.prototype);
2708$R.add('mixin', 'component', {
2709    get cmnt()
2710    {
2711        return true;
2712    }
2713});
2714$R.add('service', 'options', {
2715    init: function(options, element)
2716    {
2717        var $el = $R.dom(element);
2718        var opts = $R.extend({}, $R.opts, (element) ? $el.data() : {}, $R.options);
2719        opts = $R.extend(true, opts, options);
2720
2721        return opts;
2722    }
2723});
2724$R.add('service', 'lang', {
2725    init: function(app)
2726    {
2727        this.app = app;
2728        this.opts = app.opts;
2729
2730        // build
2731        this.vars = this._build(this.opts.lang);
2732    },
2733
2734    // public
2735    rebuild: function(lang)
2736    {
2737        this.opts.lang = lang;
2738        this.vars = this._build(lang);
2739    },
2740    extend: function(obj)
2741    {
2742        this.vars = $R.extend(this.vars, obj);
2743    },
2744    parse: function(str)
2745    {
2746        if (str === undefined)
2747        {
2748            return '';
2749        }
2750
2751        var matches = str.match(/## (.*?) ##/g);
2752        if (matches)
2753        {
2754            for (var i = 0; i < matches.length; i++)
2755            {
2756                var key = matches[i].replace(/^##\s/g, '').replace(/\s##$/g, '');
2757                str = str.replace(matches[i], this.get(key));
2758            }
2759        }
2760
2761        return str;
2762    },
2763    get: function(name)
2764    {
2765        var str = '';
2766        if (typeof this.vars[name] !== 'undefined')
2767        {
2768            str = this.vars[name];
2769        }
2770        else if (this.opts.lang !== 'en' && typeof $R.lang['en'][name] !== 'undefined')
2771        {
2772            str = $R.lang['en'][name];
2773        }
2774
2775        return str;
2776    },
2777
2778    // private
2779    _build: function(lang)
2780    {
2781        var vars = $R.lang['en'];
2782        if (lang !== 'en')
2783        {
2784            vars = ($R.lang[lang] !== undefined) ? $R.lang[lang] : vars;
2785        }
2786
2787        return vars;
2788    }
2789});
2790$R.add('service', 'callback', {
2791    init: function(app)
2792    {
2793        this.app = app;
2794        this.opts = app.opts;
2795
2796        // local
2797        this.callbacks = {};
2798
2799        // build
2800        if (this.opts.callbacks)
2801        {
2802            this._set(this.opts.callbacks, '');
2803        }
2804    },
2805    stop: function()
2806    {
2807        this.callbacks = {};
2808    },
2809    add: function(name, handler)
2810    {
2811        if (!this.callbacks[name]) this.callbacks[name] = [];
2812        this.callbacks[name].push(handler);
2813    },
2814    remove: function(name, handler)
2815    {
2816        if (handler === undefined)
2817        {
2818            delete this.callbacks[name];
2819        }
2820        else
2821        {
2822            for (var i = 0; i < this.callbacks[name].length; i++)
2823            {
2824                this.callbacks[name].splice(i, 1);
2825            }
2826
2827            if (Object.keys(this.callbacks[name]).length === 0) delete this.callbacks[name];
2828        }
2829    },
2830    trigger: function(name, args)
2831    {
2832        var value = this._loop(name, args, this.callbacks);
2833        return (typeof value === 'undefined' && args && args[0] !== false) ? args[0] : value;
2834    },
2835
2836    // private
2837    _set: function(obj, name)
2838    {
2839        for (var key in obj)
2840        {
2841            var path = (name === '') ? key : name + '.' + key;
2842            if (typeof obj[key] === 'object')
2843            {
2844                this._set(obj[key], path);
2845            }
2846            else
2847            {
2848                this.callbacks[path] = [];
2849                this.callbacks[path].push(obj[key]);
2850            }
2851        }
2852    },
2853    _loop: function(name, args, obj)
2854    {
2855        var value;
2856        for (var key in obj)
2857        {
2858            if (name === key)
2859            {
2860                for (var i = 0; i < obj[key].length; i++)
2861                {
2862                    value = obj[key][i].apply(this.app, args);
2863
2864                }
2865            }
2866        }
2867
2868        return value;
2869    }
2870});
2871$R.add('service', 'animate', {
2872    init: function(app)
2873    {
2874        this.animationOpt = app.opts.animation;
2875    },
2876    start: function(element, animation, options, callback)
2877    {
2878        var defaults = {
2879            duration: false,
2880            iterate: false,
2881            delay: false,
2882            timing: false,
2883            prefix: 'redactor-'
2884        };
2885
2886        defaults = (typeof options === 'function') ? defaults : $R.extend(defaults, options);
2887        callback = (typeof options === 'function') ? options : callback;
2888
2889        // play
2890        return new $R.AnimatePlay(element, animation, defaults, callback, this.animationOpt);
2891    },
2892    stop: function(element)
2893    {
2894        this.$el = $R.dom(element);
2895        this.$el.removeClass('redactor-animated');
2896
2897        var effect = this.$el.attr('redactor-animate-effect');
2898        this.$el.removeClass(effect);
2899
2900        this.$el.removeAttr('redactor-animate-effect');
2901        var hide = this.$el.attr('redactor-animate-hide');
2902        if (hide)
2903        {
2904            this.$el.addClass(hide).removeAttr('redactor-animate-hide');
2905        }
2906
2907        this.$el.off('animationend webkitAnimationEnd');
2908    }
2909});
2910
2911$R.AnimatePlay = function(element, animation, defaults, callback, animationOpt)
2912{
2913    this.hidableEffects = ['fadeOut', 'flipOut', 'slideUp', 'zoomOut', 'slideOutUp', 'slideOutRight', 'slideOutLeft'];
2914    this.prefixes = ['', '-webkit-'];
2915
2916    this.$el = $R.dom(element);
2917    this.$body = $R.dom('body');
2918    this.callback = callback;
2919    this.animation = (!animationOpt) ? this.buildAnimationOff(animation) : animation;
2920    this.defaults = defaults;
2921
2922    if (this.animation === 'slideUp')
2923    {
2924        this.$el.height(this.$el.height());
2925    }
2926
2927    // animate
2928    return (this.isInanimate()) ? this.inanimate() : this.animate();
2929};
2930
2931$R.AnimatePlay.prototype = {
2932    buildAnimationOff: function(animation)
2933    {
2934        return (this.isHidable(animation)) ? 'hide' : 'show';
2935    },
2936    buildHideClass: function()
2937    {
2938        return 'redactor-animate-hide';
2939    },
2940    isInanimate: function()
2941    {
2942        return (this.animation === 'show' || this.animation === 'hide');
2943    },
2944    isAnimated: function()
2945    {
2946        return this.$el.hasClass('redactor-animated');
2947    },
2948    isHidable: function(effect)
2949    {
2950        return (this.hidableEffects.indexOf(effect) !== -1);
2951    },
2952    inanimate: function()
2953    {
2954        this.defaults.timing = 'linear';
2955
2956        var hide;
2957        if (this.animation === 'show')
2958        {
2959            hide = this.buildHideClass();
2960            this.$el.attr('redactor-animate-hide', hide);
2961            this.$el.removeClass(hide);
2962        }
2963        else
2964        {
2965            hide = this.$el.attr('redactor-animate-hide');
2966            this.$el.addClass(hide).removeAttr('redactor-animate-hide');
2967        }
2968
2969        if (typeof this.callback === 'function') this.callback(this);
2970
2971        return this;
2972    },
2973    animate: function()
2974    {
2975        var delay = (this.defaults.delay) ? this.defaults.delay : 0;
2976        setTimeout(function()
2977        {
2978            this.$body.addClass('no-scroll-x');
2979            this.$el.addClass('redactor-animated');
2980            if (!this.$el.attr('redactor-animate-hide'))
2981            {
2982                var hide = this.buildHideClass();
2983                this.$el.attr('redactor-animate-hide', hide);
2984                this.$el.removeClass(hide);
2985            }
2986
2987            this.$el.addClass(this.defaults.prefix + this.animation);
2988            this.$el.attr('redactor-animate-effect', this.defaults.prefix + this.animation);
2989
2990            this.set(this.defaults.duration + 's', this.defaults.iterate, this.defaults.timing);
2991            this.complete();
2992
2993        }.bind(this), delay * 1000);
2994
2995        return this;
2996    },
2997    set: function(duration, iterate, timing)
2998    {
2999        var len = this.prefixes.length;
3000
3001        while (len--)
3002        {
3003            if (duration !== false || duration === '') this.$el.css(this.prefixes[len] + 'animation-duration', duration);
3004            if (iterate !== false || iterate === '') this.$el.css(this.prefixes[len] + 'animation-iteration-count', iterate);
3005            if (timing !== false || timing === '') this.$el.css(this.prefixes[len] + 'animation-timing-function', timing);
3006        }
3007    },
3008    clean: function()
3009    {
3010        this.$body.removeClass('no-scroll-x');
3011        this.$el.removeClass('redactor-animated');
3012        this.$el.removeClass(this.defaults.prefix + this.animation);
3013        this.$el.removeAttr('redactor-animate-effect');
3014
3015        this.set('', '', '');
3016    },
3017    complete: function()
3018    {
3019        this.$el.one('animationend webkitAnimationEnd', function()
3020        {
3021            if (this.$el.hasClass(this.defaults.prefix + this.animation)) this.clean();
3022            if (this.isHidable(this.animation))
3023            {
3024                var hide = this.$el.attr('redactor-animate-hide');
3025                this.$el.addClass(hide).removeAttr('redactor-animate-hide');
3026            }
3027
3028            if (this.animation === 'slideUp') this.$el.height('');
3029            if (typeof this.callback === 'function') this.callback(this.$el);
3030
3031        }.bind(this));
3032    }
3033};
3034$R.add('service', 'caret', {
3035    init: function(app)
3036    {
3037        this.app = app;
3038    },
3039
3040    // set
3041    setStart: function(el)
3042    {
3043        this._setCaret('Start', el);
3044    },
3045    setEnd: function(el)
3046    {
3047        this._setCaret('End', el);
3048    },
3049    setBefore: function(el)
3050    {
3051        this._setCaret('Before', el);
3052    },
3053    setAfter: function(el)
3054    {
3055        this._setCaret('After', el);
3056    },
3057
3058    // is
3059    isStart: function(el)
3060    {
3061        return this._isStartOrEnd(el, 'First');
3062    },
3063    isEnd: function(el)
3064    {
3065        return this._isStartOrEnd(el, 'Last');
3066    },
3067
3068    // set side
3069    setAtEnd: function(node)
3070    {
3071        var data = this.inspector.parse(node);
3072        var tag = data.getTag();
3073        var range = document.createRange();
3074        if (this._isInPage(node))
3075        {
3076            if (tag === 'a')
3077            {
3078                var textNode = this.utils.createInvisibleChar();
3079                $R.dom(node).after(textNode);
3080
3081                range.selectNodeContents(textNode);
3082                range.collapse(true);
3083            }
3084            else
3085            {
3086                range.selectNodeContents(node);
3087                range.collapse(false);
3088            }
3089
3090            this.selection.setRange(range);
3091        }
3092    },
3093    setAtStart: function(node)
3094    {
3095		var range = document.createRange();
3096		var data = this.inspector.parse(node);
3097        if (this._isInPage(node))
3098        {
3099            range.setStart(node, 0);
3100            range.collapse(true);
3101
3102            if (data.isInline())
3103            {
3104                var textNode = this.utils.createInvisibleChar();
3105                range.insertNode(textNode);
3106                range.selectNodeContents(textNode);
3107                range.collapse(false);
3108            }
3109
3110            this.selection.setRange(range);
3111        }
3112    },
3113    setAtBefore: function(node)
3114    {
3115        var data = this.inspector.parse(node);
3116        var range = document.createRange();
3117        if (this._isInPage(node))
3118        {
3119            range.setStartBefore(node);
3120            range.collapse(true);
3121
3122            if (data.isInline())
3123            {
3124                var textNode = this.utils.createInvisibleChar();
3125                node.parentNode.insertBefore(textNode, node);
3126                range.selectNodeContents(textNode);
3127                range.collapse(false);
3128            }
3129
3130            this.selection.setRange(range);
3131        }
3132    },
3133    setAtAfter: function(node)
3134    {
3135
3136        var range = document.createRange();
3137        if (this._isInPage(node))
3138        {
3139            range.setStartAfter(node);
3140            range.collapse(true);
3141
3142            var textNode = this.utils.createInvisibleChar();
3143            range.insertNode(textNode);
3144            range.selectNodeContents(textNode);
3145            range.collapse(false);
3146
3147            this.selection.setRange(range);
3148        }
3149    },
3150    setAtPrev: function(node)
3151    {
3152        var prev = node.previousSibling;
3153        if (prev)
3154        {
3155            prev = (prev.nodeType === 3 && this._isEmptyTextNode(prev)) ? prev.previousElementSibling : prev;
3156            if (prev) this.setEnd(prev);
3157        }
3158    },
3159    setAtNext: function(node)
3160    {
3161        var next = node.nextSibling;
3162        if (next)
3163        {
3164            next = (next.nodeType === 3 && this._isEmptyTextNode(next)) ? next.nextElementSibling : next;
3165            if (next) this.setStart(next);
3166        }
3167    },
3168
3169    // private
3170    _setCaret: function(type, el)
3171    {
3172        var data = this.inspector.parse(el);
3173        var node = data.getNode();
3174
3175        if (node)
3176        {
3177            this.component.clearActive();
3178            this['_set' + type](node, data, data.getTag());
3179        }
3180    },
3181    _setStart: function(node, data, tag)
3182    {
3183        // 1. text
3184        if (data.isText())
3185        {
3186            this.editor.focus();
3187            return this.setAtStart(node);
3188        }
3189        // 2. ul, ol
3190        else if (tag === 'ul' || tag === 'ol')
3191        {
3192            node = data.findFirstNode('li');
3193
3194            var item = this.utils.getFirstElement(node);
3195            var dataItem = this.inspector.parse(item);
3196            if (item && dataItem.isComponent())
3197            {
3198                return this.setStart(dataItem.getComponent());
3199            }
3200        }
3201        // 3. dl
3202        else if (tag === 'dl')
3203        {
3204            node = data.findFirstNode('dt');
3205        }
3206        // 4. br / hr
3207        else if (tag === 'br' || tag === 'hr')
3208        {
3209            return this.setBefore(node);
3210        }
3211        // 5. th, td
3212        else if (tag === 'td' || tag === 'th')
3213        {
3214            var el = data.getFirstElement(node);
3215            if (el)
3216            {
3217                return this.setStart(el);
3218            }
3219        }
3220        // 6. table
3221        else if (tag === 'table' || tag === 'tr')
3222        {
3223            return this.setStart(data.findFirstNode('th, td'));
3224        }
3225        // 7. figure code
3226        else if (data.isComponentType('code') && !data.isFigcaption())
3227        {
3228            var code = data.findLastNode('pre, code');
3229
3230            this.editor.focus();
3231            return this.setAtStart(code);
3232        }
3233        // 8. table component
3234        else if (tag === 'figure' && data.isComponentType('table'))
3235        {
3236            var table = data.getTable();
3237            var tableData = this.inspector.parse(table);
3238
3239            return this.setStart(tableData.findFirstNode('th, td'));
3240        }
3241        // 9. non editable components
3242        else if (!data.isComponentType('table') && data.isComponent() && !data.isFigcaption())
3243        {
3244            return this.component.setActive(node);
3245        }
3246
3247        this.editor.focus();
3248
3249        // set
3250        if (!this._setInline(node, 'Start'))
3251        {
3252            this.setAtStart(node);
3253        }
3254    },
3255    _setEnd: function(node, data, tag)
3256    {
3257        // 1. text
3258        if (data.isText())
3259        {
3260            this.editor.focus();
3261            return this.setAtEnd(node);
3262        }
3263        // 2. ul, ol
3264        else if (tag === 'ul' || tag === 'ol')
3265        {
3266            node = data.findLastNode('li');
3267
3268            var item = this.utils.getLastElement(node);
3269            var dataItem = this.inspector.parse(item);
3270            if (item && dataItem.isComponent())
3271            {
3272                return this.setEnd(dataItem.getComponent());
3273            }
3274        }
3275        // 3. dl
3276        else if (tag === 'dl')
3277        {
3278            node = data.findLastNode('dd');
3279        }
3280        // 4. br / hr
3281        else if (tag === 'br' || tag === 'hr')
3282        {
3283            return this.setAfter(node);
3284        }
3285        // 5. th, td
3286        else if (tag === 'td' || tag === 'th')
3287        {
3288            var el = data.getLastElement();
3289            if (el)
3290            {
3291                return this.setEnd(el);
3292            }
3293        }
3294        // 6. table
3295        else if (tag === 'table' || tag === 'tr')
3296        {
3297            return this.setEnd(data.findLastNode('th, td'));
3298        }
3299        // 7. figure code
3300        else if (data.isComponentType('code') && !data.isFigcaption())
3301        {
3302            var code = data.findLastNode('pre, code');
3303
3304            this.editor.focus();
3305            return this.setAtEnd(code);
3306        }
3307        // 8. table component
3308        else if (tag === 'figure' && data.isComponentType('table'))
3309        {
3310            var table = data.getTable();
3311            var tableData = this.inspector.parse(table);
3312
3313            return this.setEnd(tableData.findLastNode('th, td'));
3314        }
3315        // 9. non editable components
3316        else if (!data.isComponentType('table') && data.isComponent() && !data.isFigcaption())
3317        {
3318            return this.component.setActive(node);
3319        }
3320
3321        this.editor.focus();
3322
3323        // set
3324        if (!this._setInline(node, 'End'))
3325        {
3326            // is element empty
3327            if (this.utils.isEmpty(node))
3328            {
3329                return this.setStart(node);
3330            }
3331
3332            this.setAtEnd(node);
3333        }
3334    },
3335    _setBefore: function(node, data, tag)
3336    {
3337        // text
3338        if (node.nodeType === 3)
3339        {
3340            return this.setAtBefore(node);
3341        }
3342        // inline
3343        else if (data.isInline())
3344        {
3345            return this.setAtBefore(node);
3346        }
3347        // td / th
3348        else if (data.isFirstTableCell())
3349        {
3350            return this.setAtPrev(data.getComponent());
3351        }
3352        else if (tag === 'td' || tag === 'th')
3353        {
3354            return this.setAtPrev(node);
3355        }
3356        // li
3357        else if (data.isFirstListItem())
3358        {
3359            return this.setAtPrev(data.getList());
3360        }
3361        // figcaption
3362        else if (data.isFigcaption())
3363        {
3364            return this.setStart(data.getComponent());
3365        }
3366        // component
3367        else if (!data.isComponentType('table') && data.isComponent())
3368        {
3369            return this.setAtPrev(data.getComponent());
3370        }
3371        // block
3372        else if (data.isBlock())
3373        {
3374            return this.setAtPrev(node);
3375        }
3376
3377        this.editor.focus();
3378        this.setAtBefore(node);
3379
3380    },
3381    _setAfter: function(node, data, tag)
3382    {
3383        // text
3384        if (node.nodeType === 3)
3385        {
3386            return this.setAtAfter(node);
3387        }
3388        // inline
3389        else if (data.isInline())
3390        {
3391            return this.setAtAfter(node);
3392        }
3393        // td / th
3394        else if (data.isLastTableCell())
3395        {
3396            return this.setAtNext(data.getComponent());
3397        }
3398        else if (tag === 'td' || tag === 'th')
3399        {
3400            return this.setAtNext(node);
3401        }
3402        // li
3403        else if (data.isFirstListItem())
3404        {
3405            return this.setAtNext(data.getList());
3406        }
3407        // component
3408        else if (!data.isComponentType('table') && data.isComponent())
3409        {
3410            return this.setAtNext(data.getComponent());
3411        }
3412        // block
3413        else if (data.isBlock())
3414        {
3415            return this.setAtNext(node);
3416        }
3417
3418        this.editor.focus();
3419        this.setAtAfter(node);
3420    },
3421    _setInline: function(node, type)
3422    {
3423        // is first element inline (FF only)
3424        var inline = this._hasInlineChild(node, (type === 'Start') ? 'first' : 'last');
3425        if (inline)
3426        {
3427            if (type === 'Start')
3428            {
3429                this.setStart(inline);
3430            }
3431            else
3432            {
3433                this.setEnd(inline);
3434            }
3435
3436            return true;
3437        }
3438    },
3439    _isStartOrEnd: function(el, type)
3440    {
3441        var node = this.utils.getNode(el);
3442        if (!node) return false;
3443
3444        var data = this.inspector.parse(node);
3445        node = this._getStartEndNode(node, data, type);
3446
3447        if (node && (node.nodeType !== 3 && node.tagName !== 'LI'))
3448        {
3449            var html = (node.nodeType === 3) ? node.textContent : node.innerHTML;
3450            html = this.utils.trimSpaces(html);
3451            if (html === '') return true;
3452        }
3453
3454        if (!data.isFigcaption() && data.isComponent() && !data.isComponentEditable())
3455        {
3456            return true;
3457        }
3458
3459        var offset = this.offset.get(node, true);
3460        if (offset)
3461        {
3462            return (type === 'First') ? (offset.start === 0) : (offset.end === this.offset.size(node, true));
3463        }
3464        else
3465        {
3466            return false;
3467        }
3468    },
3469    _isInPage: function(node)
3470    {
3471        if (node && node.nodeType)
3472        {
3473            return (node === document.body) ? false : document.body.contains(node);
3474        }
3475        else
3476        {
3477            return false;
3478        }
3479    },
3480    _hasInlineChild: function(el, pos)
3481    {
3482        var data = this.inspector.parse(el);
3483        var node = (pos === 'first') ? data.getFirstNode() : data.getLastNode();
3484        var $node = $R.dom(node);
3485
3486        if (node && node.nodeType !== 3
3487            && this.inspector.isInlineTag(node.tagName)
3488            && !$node.hasClass('redactor-component')
3489             && !$node.hasClass('non-editable'))
3490        {
3491            return node;
3492        }
3493    },
3494    _isEmptyTextNode: function(node)
3495    {
3496        var text = node.textContent.trim().replace(/\n/, '');
3497        text = this.utils.removeInvisibleChars(text);
3498
3499        return (text === '');
3500    },
3501    _getStartEndNode: function(node, data, type)
3502    {
3503        if (data.isFigcaption())
3504        {
3505            node = data.getFigcaption();
3506        }
3507        else if (data.isTable())
3508        {
3509            node = data['find' + type + 'Node']('th, td');
3510        }
3511        else if (data.isList())
3512        {
3513            node = data['find' + type + 'Node']('li');
3514        }
3515        else if (data.isComponentType('code'))
3516        {
3517            node = data.findLastNode('pre, code');
3518        }
3519
3520        return node;
3521    }
3522});
3523var containsNode = function containsNode(node) {
3524    return document.getSelection().containsNode(node, true);
3525};
3526
3527var containsNodePolyfill = function polyfill(node) {
3528    var selection = document.getSelection();
3529    var start = selection.anchorNode.parentNode;
3530    var finalnode = selection.focusNode.parentNode;
3531    var rectSelection = selection.getRangeAt(0).getBoundingClientRect();
3532    var rectBlock = node.getBoundingClientRect();
3533
3534    if ($R.dom(start).closest(node).length) {
3535        return true;
3536    }
3537
3538    if ($R.dom(finalnode).closest(node).length) {
3539        return true;
3540    }
3541
3542    if (rectSelection.top < rectBlock.top && rectSelection.height > rectBlock.height) {
3543        return true;
3544    }
3545
3546    return false;
3547};
3548
3549if (!('containsNode' in Selection.prototype)) {
3550    containsNode = containsNodePolyfill;
3551}
3552
3553$R.add('service', 'selection', {
3554    init: function(app)
3555    {
3556        this.app = app;
3557    },
3558    // is
3559    is: function()
3560    {
3561        var sel = this.get();
3562        if (sel)
3563        {
3564            var node = sel.anchorNode;
3565
3566            return ($R.dom(node).closest('.redactor-in-' + this.uuid).length !== 0 || node === this.editor.getElement().get());
3567        }
3568
3569        return false;
3570    },
3571    isCollapsed: function()
3572    {
3573        var sel = this.get();
3574        var range = this.getRange();
3575
3576        if (sel && sel.isCollapsed) return true;
3577        else if (range && range.toString().length === 0) return true;
3578
3579        return false;
3580    },
3581    isBackwards: function()
3582    {
3583        var backwards = false;
3584        var sel = this.get();
3585
3586        if (sel && !sel.isCollapsed)
3587        {
3588            var range = document.createRange();
3589            range.setStart(sel.anchorNode, sel.anchorOffset);
3590            range.setEnd(sel.focusNode, sel.focusOffset);
3591            backwards = range.collapsed;
3592            range.detach();
3593        }
3594
3595        return backwards;
3596    },
3597    isIn: function(el)
3598    {
3599        var node = $R.dom(el).get();
3600        var current = this.getCurrent();
3601
3602        return (current && node) ? node.contains(current) : false;
3603    },
3604    isText: function()
3605    {
3606        var sel = this.get();
3607        if (sel)
3608        {
3609            var el = sel.anchorNode;
3610            var block = this.getBlock(el);
3611            var blocks = this.getBlocks();
3612
3613            // td, th or hasn't block
3614            if ((block && this.inspector.isTableCellTag(block.tagName)) || (block === false && blocks.length === 0))
3615            {
3616                return true;
3617            }
3618        }
3619
3620        return false;
3621    },
3622    isAll: function(el)
3623    {
3624        var node = this.utils.getNode(el);
3625        if (!node) return false;
3626
3627        var isEditor = this.editor.isEditor(node);
3628        var data = this.inspector.parse(node);
3629
3630        // component
3631        if (!data.isFigcaption() && this.component.isNonEditable(node) && this.component.isActive(node))
3632        {
3633            return true;
3634        }
3635
3636        if (isEditor)
3637        {
3638            var $editor = this.editor.getElement();
3639            var output = $editor.html().replace(/<p><\/p>$/i, '');
3640            var htmlLen = this.getHtml(false).length;
3641            var outputLen = output.length;
3642
3643            if (htmlLen !== outputLen)
3644            {
3645                return false;
3646            }
3647        }
3648
3649        // editor empty or collapsed
3650        if ((isEditor && this.editor.isEmpty()) || this.isCollapsed())
3651        {
3652            return false;
3653        }
3654
3655        // all
3656        var offset = this.offset.get(node, true);
3657        var size = this.offset.size(node, true);
3658
3659        // pre, table, or pre/code in figure
3660        if (!isEditor && data.isComponentType('code'))
3661        {
3662            size = this.getText().trim().length;
3663        }
3664
3665        if (offset && offset.start === 0 && offset.end === size)
3666        {
3667            return true;
3668        }
3669
3670        return false;
3671    },
3672
3673    // has
3674    hasNonEditable: function()
3675    {
3676        var selected = this.getHtml();
3677        var $wrapper = $R.dom('<div>').html(selected);
3678
3679        return (!this.isCollapsed() && $wrapper.find('.non-editable').length !== 0);
3680    },
3681
3682    // set
3683    setRange: function(range)
3684    {
3685        var sel = window.getSelection();
3686        sel.removeAllRanges();
3687        sel.addRange(range);
3688    },
3689    setAll: function(el)
3690    {
3691        var node = this.utils.getNode(el);
3692        if (!node) return;
3693
3694        var data = this.inspector.parse(node);
3695
3696        this.component.clearActive();
3697
3698        this.editor.focus();
3699        this.editor.saveScroll();
3700        this.editor.disableNonEditables();
3701
3702        if (node && node.tagName === 'TABLE')
3703        {
3704            var first = data.findFirstNode('td, th');
3705            var last = data.findLastNode('td, th');
3706
3707            $R.dom(first).prepend(this.marker.build('start'));
3708            $R.dom(last).append(this.marker.build('end'));
3709
3710            this.restoreMarkers();
3711        }
3712        else if (!data.isFigcaption() && this.component.isNonEditable(node))
3713        {
3714            this.component.setActive(node);
3715        }
3716        else
3717        {
3718            if (data.isComponentType('code'))
3719            {
3720                node = data.getComponentCodeElement();
3721                node.focus();
3722            }
3723
3724            var range = document.createRange();
3725            range.selectNodeContents(node);
3726
3727            this.setRange(range);
3728        }
3729
3730        this.editor.enableNonEditables();
3731        this.editor.restoreScroll();
3732    },
3733
3734    // get
3735    get: function()
3736    {
3737        var sel = window.getSelection();
3738        return (sel.rangeCount > 0) ? sel : null;
3739    },
3740    getRange: function()
3741    {
3742        var sel = this.get();
3743        return (sel) ? ((sel.getRangeAt(0)) ? sel.getRangeAt(0) : null) : null;
3744    },
3745    getTextBeforeCaret: function(num)
3746    {
3747        num = (typeof num === 'undefined') ? 1 : num;
3748
3749        var el = this.editor.getElement().get();
3750        var range = this.getRange();
3751        var text = false;
3752        if (range)
3753        {
3754            range = range.cloneRange();
3755            range.collapse(true);
3756            range.setStart(el, 0);
3757            text = range.toString().slice(-num);
3758        }
3759
3760        return text;
3761    },
3762    getTextAfterCaret: function(num)
3763    {
3764        num = (typeof num === 'undefined') ? 1 : num;
3765
3766        var el = this.editor.getElement().get();
3767        var range = this.getRange();
3768        var text = false;
3769        if (range)
3770        {
3771            var clonedRange = range.cloneRange();
3772            clonedRange.selectNodeContents(el);
3773            clonedRange.setStart(range.endContainer, range.endOffset);
3774
3775            text = clonedRange.toString().slice(0, num);
3776        }
3777
3778        return text;
3779    },
3780    getPosition: function()
3781    {
3782        var range = this.getRange();
3783        var pos = { top: 0, left: 0, width: 0, height: 0 };
3784        if (window.getSelection && range.getBoundingClientRect)
3785        {
3786            range = range.cloneRange();
3787            var offset = (range.startOffset-1);
3788            range.setStart(range.startContainer, (offset < 0) ? 0 : offset);
3789            var rect = range.getBoundingClientRect();
3790            pos = { top: rect.top, left: rect.left, width: (rect.right - rect.left) , height: (rect.bottom - rect.top) };
3791        }
3792
3793        return pos;
3794    },
3795    getCurrent: function()
3796    {
3797        var node = false;
3798        var sel = this.get();
3799        var component = this.component.getActive();
3800
3801        if (component)
3802        {
3803            node = component;
3804        }
3805        else if (sel && this.is())
3806        {
3807            node = (sel.anchorNode !== this.editor.getElement().get()) ? sel.anchorNode : false;
3808        }
3809
3810        return node;
3811    },
3812    getParent: function()
3813    {
3814        var node = false;
3815        var current = this.getCurrent();
3816        if (current)
3817        {
3818            var parent = current.parentNode;
3819            node = (parent !== this.editor.getElement().get()) ? parent : false;
3820        }
3821
3822        return node;
3823    },
3824    getElement: function(el)
3825    {
3826        var node = el || this.getCurrent();
3827        while (node)
3828        {
3829            var data = this.inspector.parse(node);
3830            if (data.isElement() && data.isInEditor())
3831            {
3832                return node;
3833            }
3834
3835            node = node.parentNode;
3836        }
3837
3838        return false;
3839    },
3840    getInline: function(el)
3841    {
3842        var node = el || this.getCurrent();
3843        var inline = false;
3844        while (node)
3845        {
3846            if (this._isInlineNode(node))
3847            {
3848                inline = node;
3849            }
3850
3851            node = node.parentNode;
3852        }
3853
3854        return inline;
3855    },
3856    getInlineFirst: function(el)
3857    {
3858        var node = el || this.getCurrent();
3859        while (node)
3860        {
3861            if (this._isInlineNode(node))
3862            {
3863                return node;
3864            }
3865
3866            node = node.parentNode;
3867        }
3868
3869        return false;
3870    },
3871    getInlineAll: function(el)
3872    {
3873        var node = el || this.getCurrent();
3874        var inlines = [];
3875        while (node)
3876        {
3877            if (this._isInlineNode(node))
3878            {
3879                inlines.push(node);
3880            }
3881
3882            node = node.parentNode;
3883        }
3884
3885        return inlines;
3886    },
3887    getBlock: function(el)
3888    {
3889        var node = el || this.getCurrent();
3890        while (node)
3891        {
3892            var data = this.inspector.parse(node);
3893            var isBlock = this.inspector.isBlockTag(node.tagName);
3894
3895            if (isBlock && data.isInEditor(node))
3896            {
3897                return node;
3898            }
3899
3900            node = node.parentNode;
3901        }
3902
3903        return false;
3904    },
3905    getInlinesAllSelected: function(options)
3906    {
3907        if (this.isAll()) return [];
3908
3909        var inlines = this.getInlines({ all: true, inside: true });
3910        //var textNodes = this.getNodes({ textnodes: true, inline: false });
3911        var selected = this.getText().replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
3912        var finalNodes = [];
3913
3914        //if (textNodes.length !== 0)
3915        // {
3916        //     return finalNodes;
3917        // }
3918
3919        if (selected === '')
3920        {
3921            finalNodes = inlines;
3922        }
3923        else if (inlines.length > 1)
3924        {
3925            for (var i = 0; i < inlines.length; i++)
3926            {
3927                if (this._isTextSelected(inlines[i], selected))
3928                {
3929                    finalNodes.push(inlines[i]);
3930                }
3931            }
3932        }
3933        else if (inlines.length === 1)
3934        {
3935            if (this._isTextSelected(inlines[0], selected))
3936            {
3937                finalNodes = inlines;
3938            }
3939        }
3940
3941        finalNodes = (options && options.tags) ? this._filterNodesByTags(finalNodes, options.tags) : finalNodes;
3942
3943        return finalNodes;
3944    },
3945    getInlines: function(options)
3946    {
3947        var nodes = this.getNodes();
3948        var filteredNodes = [];
3949        for (var i = 0; i < nodes.length; i++)
3950        {
3951            var node;
3952            if (options && options.all)
3953            {
3954                node = nodes[i];
3955                while (node)
3956                {
3957                    if (this._isInlineNode(node) && !this._isInNodesArray(filteredNodes, node))
3958                    {
3959                        filteredNodes.push(node);
3960                    }
3961
3962                    node = node.parentNode;
3963                }
3964            }
3965            else
3966            {
3967                node = this.getInline(nodes[i]);
3968                if (node && !this._isInNodesArray(filteredNodes, node))
3969                {
3970                    filteredNodes.push(node);
3971                }
3972            }
3973        }
3974
3975        // filter
3976        filteredNodes = (options && options.tags) ? this._filterNodesByTags(filteredNodes, options.tags) : filteredNodes;
3977        filteredNodes = (options && options.inside) ? this._filterInlinesInside(filteredNodes, options) : filteredNodes;
3978
3979        return filteredNodes;
3980    },
3981    getBlocks: function(options)
3982    {
3983        var nodes = this.getNodes();
3984        var block = this.getBlock();
3985        nodes = (nodes.length === 0 && block) ? [block] : nodes;
3986
3987        var filteredNodes = [];
3988        for (var i = 0; i < nodes.length; i++)
3989        {
3990            var node = this.getBlock(nodes[i]);
3991            var $node = $R.dom(node);
3992            if ($node.hasClass('non-editable')) continue;
3993
3994            if (node && !this._isInNodesArray(filteredNodes, node))
3995            {
3996                filteredNodes.push(node);
3997            }
3998        }
3999
4000        // filter
4001        filteredNodes = (options && options.tags) ? this._filterNodesByTags(filteredNodes, options.tags) : filteredNodes;
4002        filteredNodes = (options && options.first) ? this._filterBlocksFirst(filteredNodes, options) : filteredNodes;
4003
4004        return filteredNodes;
4005    },
4006    getElements: function(options)
4007    {
4008        var nodes = this.getNodes({ textnodes: false });
4009        var block = this.getBlock();
4010        nodes = (nodes.length === 0 && block) ? [block] : nodes;
4011
4012        var filteredNodes = [];
4013        for (var i = 0; i < nodes.length; i++)
4014        {
4015            if (!this._isInNodesArray(filteredNodes, nodes[i]))
4016            {
4017                filteredNodes.push(nodes[i]);
4018            }
4019        }
4020
4021        // filter
4022        filteredNodes = (options && options.tags) ? this._filterNodesByTags(filteredNodes, options.tags) : filteredNodes;
4023
4024        return filteredNodes;
4025    },
4026    getNodes: function(options)
4027    {
4028        var nodes = [];
4029        var activeComponent = this.component.getActive();
4030        if (activeComponent)
4031        {
4032            nodes = this._getNodesComponent(activeComponent);
4033        }
4034        else if (this.isCollapsed())
4035        {
4036            var current = this.getCurrent();
4037            nodes = (current) ? [current] : [];
4038        }
4039        else if (this.is() && !activeComponent)
4040        {
4041            nodes = this._getRangeSelectedNodes();
4042        }
4043
4044        // filter
4045        nodes = this._filterServicesNodes(nodes);
4046        nodes = this._filterEditor(nodes);
4047
4048        // options
4049        nodes = (options && options.tags) ? this._filterNodesByTags(nodes, options.tags) : nodes;
4050        nodes = (options && options.textnodes) ? this._filterNodesTexts(nodes, options) : nodes;
4051        nodes = (options && !options.textnodes) ? this._filterNodesElements(nodes) : nodes;
4052
4053        return nodes;
4054    },
4055
4056    // text & html
4057    getText: function()
4058    {
4059        var sel = this.get();
4060        return (sel) ? this.utils.removeInvisibleChars(sel.toString()) : '';
4061    },
4062    getHtml: function(clean)
4063    {
4064        var html = '';
4065        var sel = this.get();
4066        if (sel)
4067        {
4068            var container = document.createElement('div');
4069            var len = sel.rangeCount;
4070            for (var i = 0; i < len; ++i)
4071            {
4072                container.appendChild(sel.getRangeAt(i).cloneContents());
4073            }
4074
4075            html = container.innerHTML;
4076            html = (clean !== false) ? this.cleaner.output(html) : html;
4077            html = html.replace(/<p><\/p>$/i, '');
4078        }
4079
4080        return html;
4081    },
4082
4083    // clear
4084    clear: function()
4085    {
4086        this.component.clearActive();
4087        this.get().removeAllRanges();
4088    },
4089
4090    // collapse
4091    collapseToStart: function()
4092    {
4093        var sel = this.get();
4094        if (sel && !sel.isCollapsed) sel.collapseToStart();
4095    },
4096    collapseToEnd: function()
4097    {
4098        var sel = this.get();
4099        if (sel && !sel.isCollapsed) sel.collapseToEnd();
4100    },
4101
4102    // save
4103    saveActiveComponent: function()
4104    {
4105        var activeComponent = this.component.getActive();
4106        if (activeComponent)
4107        {
4108            this.savedComponent = activeComponent;
4109            return true;
4110        }
4111
4112        return false;
4113    },
4114    restoreActiveComponent: function()
4115    {
4116        if (this.savedComponent)
4117        {
4118            this.component.setActive(this.savedComponent);
4119            return true;
4120        }
4121
4122        return false;
4123    },
4124    save: function()
4125    {
4126        this._clearSaved();
4127
4128        var el = this.getElement();
4129        var tags = ['TD', 'TH', 'P', 'DIV', 'PRE', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'LI', 'BLOCKQUOTE'];
4130        if (el && (tags.indexOf(el.tagName) !== -1) && (el.innerHTML === '' || el.innerHTML === '<br>'))
4131        {
4132            this.savedElement = el;
4133        }
4134        else if (!this.saveActiveComponent())
4135        {
4136            this.saved = this.offset.get();
4137        }
4138    },
4139    restore: function()
4140    {
4141        if (!this.saved && !this.savedComponent && !this.savedElement) return;
4142
4143        this.editor.saveScroll();
4144        if (this.savedElement)
4145        {
4146            this.caret.setStart(this.savedElement);
4147        }
4148        else if (!this.restoreActiveComponent())
4149        {
4150            this.offset.set(this.saved);
4151        }
4152
4153        this._clearSaved();
4154        this.editor.restoreScroll();
4155    },
4156    saveMarkers: function()
4157    {
4158        this._clearSaved();
4159
4160        if (!this.saveActiveComponent())
4161        {
4162            this.marker.insert();
4163        }
4164    },
4165    restoreMarkers: function()
4166    {
4167        this.editor.saveScroll();
4168
4169        if (!this.restoreActiveComponent())
4170        {
4171            this.marker.restore();
4172        }
4173
4174        this._clearSaved();
4175        this.editor.restoreScroll();
4176    },
4177
4178    // private
4179    _getNextNode: function(node)
4180    {
4181        if (node.hasChildNodes()) return node.firstChild;
4182
4183        while (node && !node.nextSibling)
4184        {
4185            node = node.parentNode;
4186        }
4187
4188        if (!node) return null;
4189
4190        return node.nextSibling;
4191    },
4192    _getNodesComponent: function(component)
4193    {
4194        var current = this.getCurrent();
4195        var data = this.inspector.parse(current);
4196
4197        return (data.isFigcaption()) ? [data.getFigcaption()] : [component];
4198    },
4199    _getRangeSelectedNodes: function()
4200    {
4201        var nodes = [];
4202        var range = this.getRange();
4203        var node = range.startContainer;
4204        var startNode = range.startContainer;
4205        var endNode = range.endContainer;
4206        var $editor = this.editor.getElement();
4207
4208        // editor
4209        if (startNode === $editor.get() && this.isAll())
4210        {
4211            nodes = this.utils.getChildNodes($editor);
4212        }
4213        // single node
4214        else if (node === endNode)
4215        {
4216            nodes = [node];
4217        }
4218        else
4219        {
4220            while (node && node !== endNode)
4221            {
4222                nodes.push(node = this._getNextNode(node));
4223            }
4224
4225            node = range.startContainer;
4226            while (node && node !== range.commonAncestorContainer)
4227            {
4228                nodes.unshift(node);
4229                node = node.parentNode;
4230            }
4231        }
4232
4233        return nodes;
4234    },
4235    _isInNodesArray: function(nodes, node)
4236    {
4237        return (nodes.indexOf(node) !== -1);
4238    },
4239    _filterEditor: function(nodes)
4240    {
4241        var filteredNodes = [];
4242        for (var i = 0; i < nodes.length; i++)
4243        {
4244            var data = this.inspector.parse(nodes[i]);
4245            if (data.isInEditor())
4246            {
4247                filteredNodes.push(nodes[i]);
4248            }
4249        }
4250
4251        return filteredNodes;
4252    },
4253    _filterServicesNodes: function(nodes)
4254    {
4255        var filteredNodes = [];
4256        for (var i = 0; i < nodes.length; i++)
4257        {
4258            var $el = $R.dom(nodes[i]);
4259            var skip = false;
4260
4261            if (nodes[i] && nodes[i].nodeType === 3 && this.utils.isEmpty(nodes[i])) skip = true;
4262            if ($el.hasClass('redactor-script-tag')
4263                || $el.hasClass('redactor-component-caret')
4264                || $el.hasClass('redactor-selection-marker')
4265                || $el.hasClass('non-editable')) skip = true;
4266
4267            if (!skip)
4268            {
4269                filteredNodes.push(nodes[i]);
4270            }
4271        }
4272
4273        return filteredNodes;
4274    },
4275    _filterNodesTexts: function(nodes, options)
4276    {
4277        var filteredNodes = [];
4278        for (var i = 0; i < nodes.length; i++)
4279        {
4280            if (nodes[i].nodeType === 3 || (options.keepbr && nodes[i].tagName === 'BR'))
4281            {
4282                var inline = this.getInline(nodes[i]);
4283                var isInline = (inline && options && options.inline === false);
4284                if (!isInline)
4285                {
4286                    filteredNodes.push(nodes[i]);
4287                }
4288            }
4289        }
4290
4291        return filteredNodes;
4292    },
4293    _filterNodesElements: function(nodes)
4294    {
4295        var filteredNodes = [];
4296        for (var i = 0; i < nodes.length; i++)
4297        {
4298            if (nodes[i].nodeType !== 3)
4299            {
4300                filteredNodes.push(nodes[i]);
4301            }
4302        }
4303
4304        return filteredNodes;
4305    },
4306    _filterNodesByTags: function(nodes, tags, passtexts)
4307    {
4308        var filteredNodes = [];
4309        for (var i = 0; i < nodes.length; i++)
4310        {
4311            if (passtexts && nodes[i].nodeType === 3)
4312            {
4313                filteredNodes.push(nodes[i]);
4314            }
4315            else if (nodes[i].nodeType !== 3)
4316            {
4317                var nodeTag = nodes[i].tagName.toLowerCase();
4318                if (tags.indexOf(nodeTag.toLowerCase()) !== -1)
4319                {
4320                    filteredNodes.push(nodes[i]);
4321                }
4322            }
4323        }
4324
4325        return filteredNodes;
4326    },
4327    _filterBlocksFirst: function(nodes)
4328    {
4329        var filteredNodes = [];
4330        for (var i = 0; i < nodes.length; i++)
4331        {
4332            var $node = $R.dom(nodes[i]);
4333            var parent = $node.parent().get();
4334            var isFirst = ($node.parent().hasClass('redactor-in'));
4335            var isCellParent = (parent && (parent.tagName === 'TD' || parent.tagName === 'TH'));
4336            if (isFirst || isCellParent)
4337            {
4338                filteredNodes.push(nodes[i]);
4339            }
4340        }
4341
4342        return filteredNodes;
4343    },
4344    _filterInlinesInside: function(nodes)
4345    {
4346        var filteredNodes = [];
4347        for (var i = 0; i < nodes.length; i++)
4348        {
4349            if (containsNode(nodes[i], true))
4350            {
4351                filteredNodes.push(nodes[i]);
4352            }
4353        }
4354
4355        return filteredNodes;
4356    },
4357    _isTextSelected: function(node, selected) {
4358        var text = (node.nodeType !== 9) ? this.utils.removeInvisibleChars(node.textContent) : '';
4359
4360        return (
4361            selected === text
4362            || text.search(selected) !== -1
4363            || selected.search(new RegExp('^' + this.utils.escapeRegExp(text) + '$')) !== -1
4364        );
4365    },
4366    _isInlineNode: function(node)
4367    {
4368        var data = this.inspector.parse(node);
4369
4370        return (this.inspector.isInlineTag(node.tagName) && data.isInEditor());
4371    },
4372    _clearSaved: function()
4373    {
4374        this.saved = false;
4375        this.savedComponent = false;
4376        this.savedElement = false;
4377    }
4378});
4379$R.add('service', 'element', {
4380    init: function(app)
4381    {
4382        this.app = app;
4383        this.rootElement = app.rootElement;
4384
4385        // local
4386        this.$element = {};
4387        this.type = 'inline';
4388    },
4389    start: function()
4390    {
4391        this._build();
4392        this._buildType();
4393    },
4394
4395    // public
4396    isType: function(type)
4397    {
4398        return (type === this.type);
4399    },
4400    getType: function()
4401    {
4402        return this.type;
4403    },
4404    getElement: function()
4405    {
4406        return this.$element;
4407    },
4408
4409    // private
4410    _build: function()
4411    {
4412        this.$element = $R.dom(this.rootElement);
4413    },
4414    _buildType: function()
4415    {
4416        var tag = this.$element.get().tagName;
4417
4418        this.type = (tag === 'TEXTAREA') ? 'textarea' : this.type;
4419        this.type = (tag === 'DIV') ? 'div' : this.type;
4420        this.type = (this.opts.inline) ? 'inline' : this.type;
4421    }
4422});
4423$R.add('service', 'editor', {
4424    init: function(app)
4425    {
4426        this.app = app;
4427
4428        // local
4429        this.scrolltop = false;
4430        this.pasting = false;
4431    },
4432
4433    // start
4434    start: function()
4435    {
4436        this._build();
4437    },
4438
4439    // focus
4440    focus: function()
4441    {
4442        if (!this.isFocus() && !this._isContenteditableFocus())
4443        {
4444            this.saveScroll();
4445            this.$editor.focus();
4446            this.restoreScroll();
4447        }
4448    },
4449    startFocus: function()
4450    {
4451        this.caret.setStart(this.getFirstNode());
4452    },
4453    endFocus: function()
4454    {
4455        this.caret.setEnd(this.getLastNode());
4456    },
4457
4458    // pasting
4459    isPasting: function()
4460    {
4461        return this.pasting;
4462    },
4463    enablePasting: function()
4464    {
4465        this.pasting = true;
4466    },
4467    disablePasting: function()
4468    {
4469        this.pasting = false;
4470    },
4471
4472    // scroll
4473    saveScroll: function()
4474    {
4475        this.scrolltop = this._getScrollTarget().scrollTop();
4476        if (this.opts.maxHeight) {
4477            this.scrolltopin = this.$editor.scrollTop();
4478        }
4479
4480    },
4481    restoreScroll: function()
4482    {
4483        if (this.scrolltop !== false)
4484        {
4485            this._getScrollTarget().scrollTop(this.scrolltop);
4486            this.scrolltop = false;
4487        }
4488
4489        if (this.scrolltopin) {
4490            this.$editor.scrollTop(this.scrolltopin);
4491            this.scrolltopin = false;
4492        }
4493    },
4494
4495    // non editables
4496    disableNonEditables: function()
4497    {
4498        this.$noneditables = this.$editor.find('[contenteditable=false]');
4499        this.$noneditables.attr('contenteditable', true);
4500    },
4501    enableNonEditables: function()
4502    {
4503        if (this.$noneditables)
4504        {
4505            setTimeout(function() { this.$noneditables.attr('contenteditable', false); }.bind(this), 1);
4506        }
4507    },
4508
4509    // nodes
4510    getFirstNode: function()
4511    {
4512        return this.$editor.contents()[0];
4513    },
4514    getLastNode: function()
4515    {
4516        var nodes = this.$editor.contents();
4517
4518        return nodes[nodes.length-1];
4519    },
4520
4521    // utils
4522    isSourceMode: function()
4523    {
4524        var $source = this.source.getElement();
4525
4526        return $source.hasClass('redactor-source-open');
4527    },
4528    isEditor: function(el)
4529    {
4530        var node = $R.dom(el).get();
4531
4532        return (node === this.$editor.get());
4533    },
4534    isEmpty: function(keeplists)
4535    {
4536        return this.utils.isEmptyHtml(this.$editor.html(), false, keeplists);
4537    },
4538    isFocus: function()
4539    {
4540        var $active = $R.dom(document.activeElement);
4541        var isComponentSelected = (this.$editor.find('.redactor-component-active').length !== 0);
4542
4543        return (isComponentSelected || $active.closest('.redactor-in-' + this.uuid).length !== 0);
4544    },
4545    setEmpty: function()
4546    {
4547        this.$editor.html(this.opts.emptyHtml);
4548    },
4549
4550    // element
4551    getElement: function()
4552    {
4553        return this.$editor;
4554    },
4555
4556    // private
4557    _build: function()
4558    {
4559        var $element = this.element.getElement();
4560        var editableElement = (this.element.isType('textarea')) ? '<div>' : $element.get();
4561
4562        this.$editor = $R.dom(editableElement);
4563    },
4564    _getScrollTarget: function()
4565    {
4566        var $target = this.$doc;
4567        if (this.opts.toolbarFixedTarget !== document)
4568        {
4569            $target = $R.dom(this.opts.toolbarFixedTarget);
4570        }
4571        else
4572        {
4573            $target = (this.opts.scrollTarget) ? $R.dom(this.opts.scrollTarget) : $target;
4574        }
4575
4576        return $target;
4577    },
4578    _isContenteditableFocus: function()
4579    {
4580        var block = this.selection.getBlock();
4581        var $blockParent = (block) ? $R.dom(block).closest('[contenteditable=true]').not('.redactor-in') : [];
4582
4583        return ($blockParent.length !== 0);
4584    }
4585});
4586$R.add('service', 'container', {
4587    init: function(app)
4588    {
4589        this.app = app;
4590    },
4591    // public
4592    start: function()
4593    {
4594        this._build();
4595    },
4596    getElement: function()
4597    {
4598        return this.$container;
4599    },
4600
4601    // private
4602    _build: function()
4603    {
4604        var tag = (this.element.isType('inline')) ? '<span>' : '<div>';
4605        this.$container = $R.dom(tag);
4606    }
4607});
4608$R.add('service', 'source', {
4609    init: function(app)
4610    {
4611        this.app = app;
4612
4613        // local
4614        this.$source = {};
4615        this.content = '';
4616    },
4617    // public
4618    start: function()
4619    {
4620        this._build();
4621        this._buildName();
4622        this._buildStartedContent();
4623    },
4624    getElement: function()
4625    {
4626        return this.$source;
4627    },
4628    getCode: function()
4629    {
4630        return this.$source.val();
4631    },
4632    getName: function()
4633    {
4634        return this.$source.attr('name');
4635    },
4636    getStartedContent: function()
4637    {
4638        return this.content;
4639    },
4640    setCode: function(html)
4641    {
4642        return this.insertion.set(html, true, false);
4643    },
4644    isNameGenerated: function()
4645    {
4646        return (this.name);
4647    },
4648    rebuildStartedContent: function()
4649    {
4650        this._buildStartedContent();
4651    },
4652
4653    // private
4654    _build: function()
4655    {
4656        var $element = this.element.getElement();
4657        var isTextarea = this.element.isType('textarea');
4658        var sourceElement = (isTextarea) ? $element.get() : '<textarea>';
4659
4660        this.$source = $R.dom(sourceElement);
4661    },
4662    _buildName: function()
4663    {
4664        var $element = this.element.getElement();
4665
4666        this.name = $element.attr('name');
4667        this.$source.attr('name', (this.name) ? this.name : 'content-' + this.uuid);
4668    },
4669    _buildStartedContent: function()
4670    {
4671        var $element = this.element.getElement();
4672        var content = (this.element.isType('textarea')) ? $element.val() : $element.html();
4673
4674        this.content = content.trim();
4675    }
4676});
4677$R.add('service', 'statusbar', {
4678    init: function(app)
4679    {
4680        this.app = app;
4681
4682        // local
4683        this.$statusbar = {};
4684        this.items = [];
4685    },
4686    // public
4687    start: function()
4688    {
4689        this.$statusbar = $R.dom('<ul>');
4690        this.$statusbar.attr('dir', this.opts.direction);
4691    },
4692    add: function(name, html)
4693    {
4694        return this.update(name, html);
4695    },
4696    update: function(name, html)
4697    {
4698        var $item;
4699        if (typeof this.items[name] !== 'undefined')
4700        {
4701            $item = this.items[name];
4702        }
4703        else
4704        {
4705            $item = $R.dom('<li>');
4706            this.$statusbar.append($item);
4707            this.items[name] = $item;
4708        }
4709
4710        return $item.html(html);
4711    },
4712    get: function(name)
4713    {
4714        return (this.items[name]) ? this.items[name] : false;
4715    },
4716    remove: function(name)
4717    {
4718        if (this.items[name])
4719        {
4720            this.items[name].remove();
4721            delete this.items[name];
4722        }
4723    },
4724    getItems: function()
4725    {
4726        return this.items;
4727    },
4728    removeItems: function()
4729    {
4730        this.items = {};
4731        this.$statusbar.html('');
4732    },
4733    getElement: function()
4734    {
4735        return this.$statusbar;
4736    }
4737});
4738$R.add('service', 'toolbar', {
4739    init: function(app)
4740    {
4741        this.app = app;
4742
4743        // local
4744        this.buttons = [];
4745        this.dropdownOpened = false;
4746        this.buttonsObservers = {};
4747    },
4748    // public
4749    start: function()
4750    {
4751        if (this.is())
4752        {
4753            this.opts.activeButtons = (this.opts.activeButtonsAdd) ? this._extendActiveButtons() : this.opts.activeButtons;
4754            this.create();
4755        }
4756    },
4757    stopObservers: function()
4758    {
4759        this.buttonsObservers = {};
4760    },
4761    create: function()
4762    {
4763        this.$wrapper = $R.dom('<div>');
4764        this.$toolbar = $R.dom('<div>');
4765    },
4766    observe: function()
4767    {
4768        if (!this.is()) return;
4769
4770        this.setButtonsInactive();
4771
4772        var button, observer;
4773
4774        // observers
4775        for (var name in this.buttonsObservers)
4776        {
4777            observer = this.buttonsObservers[name];
4778            button = this.getButton(name);
4779            this.app.broadcast('button.' + observer + '.observe', button);
4780        }
4781
4782        // inline buttons
4783        var buttons = this.opts.activeButtons;
4784        var inlines = this.selection.getInlinesAllSelected();
4785        var current = this.selection.getInline();
4786        var currents = this.selection.getInlineAll();
4787
4788        if (this.selection.isCollapsed())
4789        {
4790            if (current) inlines.push(current);
4791            if (currents.length !== 0) {
4792                inlines = inlines.concat(currents);
4793            }
4794        }
4795
4796        var tags = this._inlinesToTags(inlines);
4797        for (var key in buttons)
4798        {
4799            if (tags.indexOf(key) !== -1)
4800            {
4801                button = this.getButton(buttons[key]);
4802                if (button) {
4803                    button.setActive();
4804                }
4805            }
4806
4807        }
4808    },
4809
4810    // is
4811    is: function()
4812    {
4813        return !(!this.opts.toolbar || (this.detector.isMobile() && this.opts.air));
4814    },
4815    isAir: function()
4816    {
4817        return (this.is()) ? this.$toolbar.hasClass('redactor-air') : false;
4818    },
4819    isFixed: function()
4820    {
4821        return (this.is()) ? this.$toolbar.hasClass('redactor-toolbar-fixed') : false;
4822    },
4823    isContextBar: function()
4824    {
4825        var $bar = this.$body.find('#redactor-context-toolbar-' + this.uuid);
4826        return $bar.hasClass('open');
4827    },
4828    isTarget: function()
4829    {
4830        return (this.opts.toolbarFixedTarget !== document);
4831    },
4832
4833    // get
4834    getElement: function()
4835    {
4836        return this.$toolbar;
4837    },
4838    getWrapper: function()
4839    {
4840        return this.$wrapper;
4841    },
4842    getDropdown: function()
4843    {
4844        return this.dropdownOpened;
4845    },
4846    getTargetElement: function()
4847    {
4848        return $R.dom(this.opts.toolbarFixedTarget);
4849    },
4850    getButton: function(name)
4851    {
4852        var $btn = this._findButton('.re-' + name);
4853
4854        return ($btn.length !== 0) ? $btn.dataget('data-button-instance') : false;
4855    },
4856    getButtons: function()
4857    {
4858        var buttons = [];
4859        this._findButtons().each(function(node)
4860        {
4861            var $node = $R.dom(node);
4862            buttons.push($node.dataget('data-button-instance'));
4863        });
4864
4865        return buttons;
4866    },
4867    getButtonsKeys: function()
4868    {
4869        var keys = [];
4870        this._findButtons().each(function(node)
4871        {
4872            var $node = $R.dom(node);
4873            keys.push($node.attr('data-re-name'));
4874        });
4875
4876        return keys;
4877    },
4878
4879    // add
4880    addButton: function(name, btnObj, position, $el, start)
4881    {
4882        position = position || 'end';
4883
4884        var $button = $R.create('toolbar.button', this.app, name, btnObj);
4885        if (btnObj.observe)
4886        {
4887            this.opts.activeButtonsObservers[name] = { observe: btnObj.observe, button: $button };
4888        }
4889
4890        if (this.is())
4891        {
4892            if (position === 'first') this.$toolbar.prepend($button);
4893            else if (position === 'after') $el.after($button);
4894            else if (position === 'before') $el.before($button);
4895            else {
4896                var index = this.opts.buttons.indexOf(name);
4897                if (start !== true && index !== -1) {
4898                    if (index === 0) {
4899                        this.$toolbar.prepend($button);
4900                    }
4901                    else {
4902                        var $btns = this._findButtons();
4903                        var $btn = $btns.eq(index-1);
4904                        if ($btn.length > 0) {
4905                            $btn.after($button);
4906                        }
4907                        else {
4908                            this.$toolbar.append($button);
4909                        }
4910                    }
4911                }
4912                else {
4913                    this.$toolbar.append($button);
4914                }
4915            }
4916        }
4917
4918        return $button;
4919    },
4920    addButtonFirst: function(name, btnObj)
4921    {
4922        return this.addButton(name, btnObj, 'first');
4923    },
4924    addButtonAfter: function(after, name, btnObj)
4925    {
4926        var $btn = this.getButton(after);
4927
4928        return ($btn) ? this.addButton(name, btnObj, 'after', $btn) : this.addButton(name, btnObj);
4929    },
4930    addButtonBefore: function(before, name, btnObj)
4931    {
4932        var $btn = this.getButton(before);
4933
4934        return ($btn) ? this.addButton(name, btnObj, 'before', $btn) : this.addButton(name, btnObj);
4935    },
4936    addButtonObserver: function(name, observer)
4937    {
4938        this.buttonsObservers[name] = observer;
4939    },
4940
4941    // set
4942    setButtons: function(buttons)
4943    {
4944        this.buttons = buttons;
4945    },
4946    setDropdown: function(dropdown)
4947    {
4948        this.dropdownOpened = dropdown;
4949    },
4950    setButtonsInactive: function()
4951    {
4952        var $buttons = this.getButtons();
4953        for (var i = 0; i < $buttons.length; i++)
4954        {
4955            $buttons[i].setInactive();
4956        }
4957    },
4958    setButtonsActive: function()
4959    {
4960        var $buttons = this.getButtons();
4961        for (var i = 0; i < $buttons.length; i++)
4962        {
4963            $buttons[i].setActive();
4964        }
4965    },
4966
4967    // disable & enable
4968    disableButtons: function()
4969    {
4970        var $buttons = this.getButtons();
4971        for (var i = 0; i < $buttons.length; i++)
4972        {
4973            $buttons[i].disable();
4974        }
4975    },
4976    enableButtons: function()
4977    {
4978        var $buttons = this.getButtons();
4979        for (var i = 0; i < $buttons.length; i++)
4980        {
4981            $buttons[i].enable();
4982        }
4983
4984    },
4985
4986    // private
4987    _findButton: function(selector)
4988    {
4989        return (this.is()) ? this.$toolbar.find(selector) : $R.dom();
4990    },
4991    _findButtons: function()
4992    {
4993        return (this.is()) ? this.$toolbar.find('.re-button') : $R.dom();
4994    },
4995    _extendActiveButtons: function()
4996    {
4997        return $R.extend({}, this.opts.activeButtons, this.opts.activeButtonsAdd);
4998    },
4999    _inlinesToTags: function(inlines)
5000    {
5001        var tags = [];
5002        for (var i = 0; i < inlines.length; i++)
5003        {
5004            tags.push(inlines[i].tagName.toLowerCase());
5005        }
5006
5007        return tags;
5008    }
5009});
5010$R.add('class', 'toolbar.button', {
5011    mixins: ['dom'],
5012    init: function(app, name, btnObj)
5013    {
5014        this.app = app;
5015        this.opts = app.opts;
5016        this.lang = app.lang;
5017        this.$body = app.$body;
5018        this.toolbar = app.toolbar;
5019        this.detector = app.detector;
5020
5021        // local
5022        this.obj = btnObj;
5023        this.name = name;
5024        this.dropdown = false;
5025        this.tooltip = false;
5026
5027        // init
5028        this._init();
5029    },
5030    // is
5031    isActive: function()
5032    {
5033        return this.hasClass('redactor-button-active');
5034    },
5035    isDisabled: function()
5036    {
5037        return this.hasClass('redactor-button-disabled');
5038    },
5039
5040    // has
5041    hasIcon: function()
5042    {
5043        return (this.obj.icon && !this.opts.buttonsTextLabeled);
5044    },
5045
5046    // set
5047    setDropdown: function(dropdown)
5048    {
5049        this.obj.dropdown = dropdown;
5050        this.obj.message = false;
5051        this.dropdown = $R.create('toolbar.dropdown', this.app, this.name, this.obj.dropdown);
5052        this.attr('data-dropdown', true);
5053    },
5054    setMessage: function(message, args)
5055    {
5056        this.obj.message = message;
5057        this.obj.args = args;
5058        this.obj.dropdown = false;
5059    },
5060    setApi: function(api, args)
5061    {
5062        this.obj.api = api;
5063        this.obj.args = args;
5064        this.obj.dropdown = false;
5065    },
5066    setTitle: function(title)
5067    {
5068        this.obj.title = this.lang.parse(title);
5069        this.obj.tooltip = this.obj.title;
5070
5071        this.attr({ 'alt': this.obj.tooltip, 'aria-label': this.obj.tooltip });
5072        if (!this.attr('data-re-icon')) this.html(this.obj.title);
5073    },
5074    setTooltip: function(tooltip)
5075    {
5076        this.obj.tooltip = this.lang.parse(tooltip);
5077        this.attr({ 'alt': this.obj.tooltip, 'aria-label': this.obj.tooltip });
5078    },
5079    setIcon: function(icon)
5080    {
5081        if (this.opts.buttonsTextLabeled) return;
5082
5083        this.obj.icon = true;
5084        this.$icon = $R.dom(icon);
5085
5086        this.html('');
5087        this.append(this.$icon);
5088        this.attr('data-re-icon', true);
5089        this.addClass('re-button-icon');
5090        this.setTooltip(this.obj.title);
5091        this._buildTooltip();
5092    },
5093    setActive: function()
5094    {
5095        this.addClass('redactor-button-active');
5096    },
5097    setInactive: function()
5098    {
5099        this.removeClass('redactor-button-active');
5100    },
5101
5102    // hide
5103    hideTooltip: function()
5104    {
5105        this.$body.find('.re-button-tooltip').remove();
5106    },
5107
5108    // get
5109    getDropdown: function()
5110    {
5111        return this.dropdown;
5112    },
5113
5114    // enable & disable
5115    disable: function()
5116    {
5117        this.addClass('redactor-button-disabled');
5118    },
5119    enable: function()
5120    {
5121        this.removeClass('redactor-button-disabled');
5122    },
5123
5124    // toggle
5125    toggle: function(e)
5126    {
5127        if (e) e.preventDefault();
5128        if (this.isDisabled()) return;
5129
5130        if (this.obj.dropdown)
5131        {
5132            this.dropdown.toggle(e);
5133        }
5134        else if (this.obj.api)
5135        {
5136            // broadcast
5137            this.app.api(this.obj.api, this.obj.args, this.name);
5138        }
5139        else if (this.obj.message)
5140        {
5141            // broadcast
5142            this.app.broadcast(this.obj.message, this.obj.args, this.name);
5143        }
5144
5145        this.hideTooltip();
5146    },
5147
5148    // private
5149    _init: function()
5150    {
5151        // parse
5152        this._parseTitle();
5153        this._parseTooltip();
5154
5155        // build
5156        this._build();
5157        this._buildCallback();
5158        this._buildAttributes();
5159        this._buildObserver();
5160
5161        if (this.hasIcon())
5162        {
5163            this._buildIcon();
5164            this._buildTooltip();
5165        }
5166        else
5167        {
5168            this.html(this.obj.title);
5169        }
5170    },
5171    _parseTooltip: function()
5172    {
5173        this.obj.tooltip = (this.obj.tooltip) ? this.lang.parse(this.obj.tooltip) : this.obj.title;
5174    },
5175    _parseTitle: function()
5176    {
5177        this.obj.title = this.lang.parse(this.obj.title);
5178    },
5179    _build: function()
5180    {
5181        this.parse('<a>');
5182        this.addClass('re-button re-' + this.name);
5183        this.attr('data-re-name', this.name);
5184        this.dataset('data-button-instance', this);
5185
5186        if (this.obj.dropdown) this.setDropdown(this.obj.dropdown);
5187    },
5188    _buildCallback: function()
5189    {
5190        this.on('click', this.toggle.bind(this));
5191    },
5192    _buildAttributes: function()
5193    {
5194        var attrs = {
5195            'href': '#',
5196            'alt': this.obj.tooltip,
5197            'rel': this.name,
5198            'role': 'button',
5199            'aria-label': this.obj.tooltip,
5200            'tabindex': '-1'
5201        };
5202
5203        this.attr(attrs);
5204    },
5205    _buildObserver: function()
5206    {
5207        if (typeof this.obj.observe !== 'undefined')
5208        {
5209            this.toolbar.addButtonObserver(this.name, this.obj.observe);
5210        }
5211    },
5212    _buildIcon: function()
5213    {
5214        var icon = this.obj.icon;
5215        var isHtml = (/(<([^>]+)>)/ig.test(icon));
5216
5217        this.$icon = (isHtml) ? $R.dom(icon) : $R.dom('<i>');
5218        if (!isHtml) this.$icon.addClass('re-icon-' + this.name);
5219
5220        this.append(this.$icon);
5221        this.attr('data-re-icon', true);
5222        this.addClass('re-button-icon');
5223    },
5224    _buildTooltip: function()
5225    {
5226        if (this.detector.isDesktop())
5227        {
5228            this.tooltip = $R.create('toolbar.button.tooltip', this.app, this);
5229        }
5230    }
5231});
5232$R.add('class', 'toolbar.button.tooltip', {
5233    mixins: ['dom'],
5234    init: function(app, $button)
5235    {
5236        this.app = app;
5237        this.uuid = app.uuid;
5238        this.opts = app.opts;
5239        this.$body = app.$body;
5240        this.toolbar = app.toolbar;
5241
5242        // local
5243        this.$button = $button;
5244        this.created = false;
5245
5246        // init
5247        this._init();
5248    },
5249    open: function()
5250    {
5251        if (this.$button.hasClass('redactor-button-disabled') || this.$button.hasClass('redactor-button-active')) return;
5252
5253        this.created = true;
5254        this.parse('<span>');
5255        this.addClass('re-button-tooltip re-button-tooltip-' + this.uuid);
5256        this.$body.append(this);
5257        this.html(this.$button.attr('alt'));
5258
5259        var offset = this.$button.offset();
5260        var position = 'absolute';
5261        var height = this.$button.height();
5262        var width = this.$button.width();
5263        var arrowOffset = 4;
5264
5265        this.css({
5266            top: (offset.top + height + arrowOffset) + 'px',
5267            left: (offset.left + width/2 - this.width()/2) + 'px',
5268            position: position
5269        });
5270
5271        this.show();
5272    },
5273    close: function()
5274    {
5275        if (!this.created || this.$button.hasClass('redactor-button-disabled')) return;
5276
5277        this.remove();
5278        this.created = false;
5279    },
5280
5281    // private
5282    _init: function()
5283    {
5284        this.$button.on('mouseover', this.open.bind(this));
5285        this.$button.on('mouseout', this.close.bind(this));
5286    }
5287});
5288$R.add('class', 'toolbar.dropdown', {
5289    mixins: ['dom'],
5290    init: function(app, name, items)
5291    {
5292        this.app = app;
5293        this.uuid = app.uuid;
5294        this.opts = app.opts;
5295        this.$win = app.$win;
5296        this.$doc = app.$doc;
5297        this.$body = app.$body;
5298        this.animate = app.animate;
5299        this.toolbar = app.toolbar;
5300
5301        // local
5302        this.name = name;
5303        this.started = false;
5304        this.items = (name === 'format') ? $R.extend({}, true, items) : items;
5305        this.$items = [];
5306    },
5307    // public
5308    toggle: function(e)
5309    {
5310        if (!this.started)
5311        {
5312            this._build();
5313        }
5314
5315        // toggle
5316        if (this.isOpened() && this.isActive())
5317        {
5318            this.close(false);
5319        }
5320        else
5321        {
5322            this.open(e);
5323        }
5324    },
5325    isOpened: function()
5326    {
5327        var $dropdown = this.$body.find('.redactor-dropdown-' + this.uuid + '.open');
5328
5329        return ($dropdown.length !== 0 && $dropdown.attr('data-re-name') === this.name);
5330    },
5331    isActive: function()
5332    {
5333        var $dropdown = this.$body.find('#redactor-dropdown-' + this.uuid + '-' + this.name + '.open');
5334        return ($dropdown.length !== 0);
5335    },
5336    getName: function()
5337    {
5338        return this.attr('data-re-name');
5339    },
5340    getItem: function(name)
5341    {
5342        return this.$items[name];
5343    },
5344    getItemsByClass: function(classname)
5345    {
5346        var result = [];
5347        for (var key in this.$items) {
5348            var item = this.$items[key];
5349            if (typeof item === 'object' && item.attr('data-re-name') && item.hasClass(classname)) {
5350                result.push(item);
5351            }
5352        }
5353
5354        return result;
5355    },
5356    open: function(e)
5357    {
5358        this._closeAll();
5359
5360        this.$btn = this.toolbar.getButton(this.name);
5361        this.app.broadcast('dropdown.open', e, this, this.$btn);
5362        this.toolbar.setDropdown(this);
5363
5364        this.show();
5365        this.removeClass('redactor-animate-hide');
5366        this.addClass('open');
5367        this._observe();
5368
5369        this.$btn.hideTooltip();
5370        this.$btn.setActive();
5371
5372        this.$doc.on('keyup.redactor.dropdown-' + this.uuid, this._handleKeyboard.bind(this));
5373        this.$doc.on('click.redactor.dropdown-' + this.uuid, this.close.bind(this));
5374
5375        this.updatePosition();
5376        this.app.broadcast('dropdown.opened', e, this, this.$btn);
5377
5378    },
5379    close: function(e, animate)
5380    {
5381        if (e)
5382        {
5383            var $el = $R.dom(e.target);
5384            if (this._isButton(e) || $el.hasClass('redactor-dropdown-not-close') || $el.hasClass('redactor-dropdown-item-disabled'))
5385            {
5386                e.preventDefault();
5387                return;
5388            }
5389        }
5390
5391        this.app.broadcast('dropdown.close', this, this.$btn);
5392        this.toolbar.setDropdown(false);
5393
5394        this.$btn.setInactive();
5395        if (animate === false)
5396        {
5397            this._close();
5398        }
5399        else
5400        {
5401            this.animate.start(this, 'fadeOut', this._close.bind(this));
5402        }
5403    },
5404    updatePosition: function()
5405    {
5406        var isFixed = this.toolbar.isFixed();
5407        var isTarget = this.toolbar.isTarget();
5408
5409        var btnHeight = this.$btn.height();
5410        var btnWidth = this.$btn.width();
5411
5412        var pos = this.$btn.offset();
5413        var position = 'absolute';
5414        var topOffset = 2;
5415
5416
5417        if (isFixed) {
5418            //pos.top = (isTarget) ? this.$btn.offset().top : this.$btn.position().top;
5419            //position = 'fixed';
5420            //topOffset = topOffset + this.opts.toolbarFixedTopOffset;
5421        }
5422
5423        var leftOffset = 0;
5424        var left = (pos.left + leftOffset);
5425        var width = parseFloat(this.css('width'));
5426        var winWidth = this.$win.width();
5427        var leftFix = (winWidth < (left + width)) ? (width - btnWidth) : 0;
5428        var leftPos = (left - leftFix);
5429        var top = (pos.top + btnHeight + topOffset);
5430        leftPos = (leftPos < 0) ? 4 : leftPos;
5431
5432        this.css({
5433            maxHeight: '',
5434            position: position,
5435            top: top + 'px',
5436            left: leftPos + 'px'
5437        });
5438
5439        // height adaptive
5440        var heightTolerance = 10;
5441        var winHeight = this.$win.height();
5442        var scrollTop = this.$doc.scrollTop();
5443        var cropHeight = winHeight - (top - scrollTop) - heightTolerance;
5444
5445        this.css('max-height', cropHeight + 'px');
5446    },
5447
5448    // private
5449    _build: function()
5450    {
5451        this.parse('<div>');
5452        this.attr('dir', this.opts.direction);
5453        this.attr('id', 'redactor-dropdown-' + this.uuid + '-' + this.name);
5454        this.attr('data-re-name', this.name);
5455
5456        this.addClass('redactor-dropdown redactor-dropdown-' + this.uuid + ' redactor-dropdown-' + this.name);
5457        this.dataset('data-dropdown-instance', this);
5458        var isDom = (this.items.sdom || typeof this.items === 'string');
5459
5460        if (isDom) this._buildDom();
5461        else this._buildItems();
5462
5463        this.$body.append(this);
5464        this.started = true;
5465    },
5466    _buildDom: function()
5467    {
5468        this.html('').append($R.dom(this.items));
5469    },
5470    _buildItems: function()
5471    {
5472        this.items = (this.name === 'format') ? this._buildFormattingItems() : this.items;
5473
5474        for (var key in this.items)
5475        {
5476            var obj = this.items[key];
5477
5478            if (key === 'observe')
5479            {
5480                this.attr('data-observe', this.items[key]);
5481            }
5482            else
5483            {
5484                var $item = $R.create('toolbar.dropdown.item', this.app, key, obj, this);
5485
5486                this.$items[key] = $item;
5487                this.append($item);
5488            }
5489        }
5490    },
5491    _buildFormattingItems: function()
5492    {
5493        // build the format set
5494        for (var key in this.items)
5495        {
5496            if (this.opts.formatting.indexOf(key) === -1) delete this.items[key];
5497        }
5498
5499        // remove from the format set
5500        if (this.opts.formattingHide)
5501        {
5502            for (var key in this.items)
5503            {
5504                if (this.opts.formattingHide.indexOf(key) !== -1) delete this.items[key];
5505            }
5506        }
5507
5508        // add to the format set
5509        if (this.opts.formattingAdd)
5510        {
5511            for (var key in this.opts.formattingAdd)
5512            {
5513                this.items[key] = this.opts.formattingAdd[key];
5514            }
5515        }
5516
5517        return this.items;
5518    },
5519    _handleKeyboard: function(e)
5520    {
5521        if (e.which === 27) this.close();
5522    },
5523    _isButton: function(e)
5524    {
5525        var $el = $R.dom(e.target);
5526        var $btn = $el.closest('.re-button');
5527
5528        return ($btn.get() === this.$btn.get());
5529    },
5530    _close: function()
5531    {
5532        this.$btn.setInactive();
5533        this.$doc.off('.redactor.dropdown-' + this.uuid);
5534        this.removeClass('open');
5535        this.addClass('redactor-animate-hide');
5536        this.app.broadcast('dropdown.closed', this, this.$btn);
5537    },
5538    _closeAll: function()
5539    {
5540        this.$body.find('.redactor-dropdown-' + this.uuid + '.open').each(function(node)
5541        {
5542            var $node = $R.dom(node);
5543            var instance =  $node.dataget('data-dropdown-instance');
5544            instance._close();
5545        });
5546    },
5547    _observe: function()
5548    {
5549        var observer = this.attr('data-observe');
5550        if (observer)
5551        {
5552            this.app.broadcast('dropdown.' + observer + '.observe', this);
5553        }
5554    }
5555});
5556$R.add('class', 'toolbar.dropdown.item', {
5557    mixins: ['dom'],
5558    init: function(app, name, obj, dropdown)
5559    {
5560        this.app = app;
5561        this.lang = app.lang;
5562
5563        // local
5564        this.dropdown = dropdown;
5565        this.name = name;
5566        this.obj = obj;
5567
5568        // init
5569        this._init();
5570    },
5571    setTitle: function(html)
5572    {
5573        this.$span.html(html);
5574    },
5575    getTitle: function()
5576    {
5577        return this.$span.html();
5578    },
5579    enable: function()
5580    {
5581        this.removeClass('redactor-dropdown-item-disabled');
5582    },
5583    disable: function()
5584    {
5585        this.addClass('redactor-dropdown-item-disabled');
5586    },
5587    toggle: function(e)
5588    {
5589        if (e) e.preventDefault();
5590        if (this.hasClass('redactor-dropdown-item-disabled')) return;
5591
5592        if (this.obj.message)
5593        {
5594            // broadcast
5595            this.app.broadcast(this.obj.message, this.obj.args, this.name);
5596        }
5597        else if (this.obj.api)
5598        {
5599            this.app.api(this.obj.api, this.obj.args, this.name);
5600        }
5601    },
5602
5603    // private
5604    _init: function()
5605    {
5606        this.parse('<a>');
5607        this.attr('href', '#');
5608        this.addClass('redactor-dropdown-item-' + this.name);
5609
5610        if (this.obj.classname)
5611        {
5612            this.addClass(this.obj.classname);
5613        }
5614
5615        this.attr('data-re-name', this.name);
5616        this.on('click', this.toggle.bind(this));
5617
5618        this.$span = $R.dom('<span>');
5619        this.append(this.$span);
5620        this.setTitle(this.lang.parse(this.obj.title));
5621    }
5622});
5623$R.add('service', 'cleaner', {
5624    init: function(app)
5625    {
5626        this.app = app;
5627        this.opts = app.opts;
5628
5629        // local
5630        this.storedComponents = [];
5631        this.storedComments = [];
5632        this.storedImages = [];
5633        this.storedLinks = [];
5634        this.deniedTags = ['font', 'html', 'head', 'link', 'title', 'body', 'meta', 'applet'];
5635        this.convertRules = {};
5636        this.unconvertRules = {};
5637
5638        // regex
5639        this.reComments = /<!--[\s\S]*?-->\n?/g;
5640        this.reSpacedEmpty = /^(||\s||<br\s?\/?>||&nbsp;)$/i;
5641        this.reScriptTag = /<script(.*?[^>]?)>([\w\W]*?)<\/script>/gi;
5642    },
5643    // public
5644    addConvertRules: function(name, func)
5645    {
5646        this.convertRules[name] = func;
5647    },
5648    addUnconvertRules: function(name, func)
5649    {
5650        this.unconvertRules[name] = func;
5651    },
5652    input: function(html, paragraphize, started)
5653    {
5654        // fix &curren; entity in the links
5655        html = html.replace(/¤t/gi, '&current');
5656
5657        // store
5658        var storedComments = [];
5659        html = this.storeComments(html, storedComments);
5660
5661        // pre/code
5662        html = this.encodeCode(html);
5663
5664        // sanitize
5665        var $wrapper = this.utils.buildWrapper(html);
5666        $wrapper.find('a, b, i, strong, em, img, svg, details, audio').removeAttr('onload onerror ontoggle onwheel onmouseover oncopy');
5667        $wrapper.find('a, iframe, embed').each(function(node) {
5668            var $node = $R.dom(node);
5669            var href = $node.attr('href');
5670            var src = $node.attr('src');
5671            if (href && href.trim().search(/^data|javascript:/i) !== -1) $node.attr('href', '');
5672            if (src && src.trim().search(/^data|javascript:/i) !== -1) $node.attr('src', '');
5673        });
5674
5675        var imageattrs = ['alt', 'title', 'src', 'class', 'width', 'height', 'srcset', 'style', 'usemap'];
5676        $wrapper.find('img').each(function(node) {
5677            if (node.attributes.length > 0) {
5678                var attrs = node.attributes;
5679                for (var i = attrs.length - 1; i >= 0; i--) {
5680                    var removeAttrs = (attrs[i].name.search(/^data-/) === -1 && imageattrs.indexOf(attrs[i].name) === -1);
5681                    var removeDataSrc = (attrs[i].name === 'src' && attrs[i].value.search(/^data|javascript:/i) !== -1);
5682                    if (this.opts.imageSrcData) removeDataSrc = false;
5683
5684                    if (removeAttrs || removeDataSrc) {
5685                        node.removeAttribute(attrs[i].name);
5686                    }
5687                }
5688            }
5689        }.bind(this));
5690
5691        // get wrapper html
5692        html = this.utils.getWrapperHtml($wrapper);
5693
5694        // converting entity
5695        html = html.replace(/\$/g, '&#36;');
5696        html = html.replace(/&amp;/g, '&');
5697
5698        // convert to figure
5699        var converter = $R.create('cleaner.figure', this.app);
5700        html = converter.convert(html, this.convertRules);
5701
5702        // store components
5703        html = this.storeComponents(html);
5704
5705        // clean
5706        html = this.replaceTags(html, this.opts.replaceTags);
5707        html = this._setSpanAttr(html);
5708        html = this._setStyleCache(html);
5709        html = this.removeTags(html, this.deniedTags);
5710        html = (this.opts.removeScript) ? this._removeScriptTag(html) : this._replaceScriptTag(html);
5711        //html = (this.opts.removeScript) ? this._removeScriptTag(html) : html;
5712        html = (this.opts.removeComments) ? this.removeComments(html) : html;
5713        html = (this._isSpacedEmpty(html)) ? this.opts.emptyHtml : html;
5714
5715
5716        // restore components
5717        html = this.restoreComponents(html);
5718
5719
5720        // clear wrapped components
5721        html = this._cleanWrapped(html);
5722
5723        // restore comments
5724        html = this.restoreComments(html, storedComments);
5725
5726        // paragraphize
5727        html = (paragraphize) ? this.paragraphize(html) : html;
5728
5729        return html;
5730    },
5731    output: function(html, removeMarkers)
5732    {
5733        html = this.removeInvisibleSpaces(html);
5734
5735        if (this.opts.breakline) {
5736            html = html.replace(/<\/(span|strong|b|i|em)><br\s?\/?><\/div>/gi, "</$1></div>");
5737            html = html.replace(/<br\s?\/?><\/(span|strong|b|i|em)><\/div>/gi, "</$1></div>");
5738        }
5739
5740        html = html.replace(/&#36;/g, '$');
5741
5742        // empty
5743        if (this._isSpacedEmpty(html)) return '';
5744        if (this._isParagraphEmpty(html)) return '';
5745
5746        html = this.removeServiceTagsAndAttrs(html, removeMarkers);
5747
5748        // store components
5749        html = this.storeComponents(html);
5750
5751        html = this.removeSpanWithoutAttributes(html);
5752        html = this.removeFirstBlockBreaklineInHtml(html);
5753
5754        html = (this.opts.removeScript) ? html : this._unreplaceScriptTag(html);
5755        html = (this.opts.preClass) ? this._setPreClass(html) : html;
5756        html = (this.opts.linkNofollow) ? this._setLinkNofollow(html) : html;
5757        html = (this.opts.removeNewLines) ? this.cleanNewLines(html) : html;
5758
5759        // restore components
5760        html = this.restoreComponents(html);
5761
5762        // convert to figure
5763        var converter = $R.create('cleaner.figure', this.app);
5764        html = converter.unconvert(html, this.unconvertRules);
5765
5766        // final clean up
5767        html = this.removeEmptyAttributes(html, ['style', 'class', 'rel', 'alt', 'title']);
5768        html = this.cleanSpacesInPre(html);
5769        html = this.tidy(html);
5770
5771        // converting entity
5772        html = html.replace(/&amp;/g, '&');
5773
5774        // breakline tidy
5775        if (this.opts.breakline) {
5776            html = html.replace(/<br\s?\/?>/gi, "<br>\n");
5777            html = html.replace(/<br\s?\/?>\n+/gi, "<br>\n");
5778        }
5779
5780        // check whitespaces
5781        html = (html.replace(/\n/g, '') === '') ? '' : html;
5782
5783        return html;
5784    },
5785    paste: function(html)
5786    {
5787        // store components
5788        html = this.storeComponents(html);
5789
5790        // remove comments
5791        html = html.replace(/<!--[\s\S]*?-->/g, '');
5792
5793        // remove tags
5794        var deniedTags = this.deniedTags.concat(['iframe']);
5795        html = this.removeTags(html, deniedTags);
5796
5797        // remove doctype tag
5798        html = html.replace(new RegExp("<!doctype([\\s\\S]+?)>", 'gi'), '');
5799
5800        // remove style tag
5801        html = html.replace(new RegExp("<style([\\s\\S]+?)</style>", 'gi'), '');
5802
5803        // remove br between
5804        html = html.replace(new RegExp("</p><br /><p", 'gi'), '</p><p');
5805
5806        // gdocs & word
5807        var isMsWord = this._isHtmlMsWord(html);
5808
5809        html = (isMsWord) ? html : this._cleanGDocs(html);
5810        html = (isMsWord) ? this._cleanMsWord(html) : html;
5811
5812        // do not clean
5813        if (!this.opts.pasteClean)
5814        {
5815            // restore components
5816            html = this.restoreComponents(html);
5817
5818            return html;
5819        }
5820
5821        // plain text
5822        if (this.opts.pastePlainText)
5823        {
5824            // restore components
5825            html = this.restoreComponents(html);
5826
5827            return this.pastePlainText(html);
5828        }
5829
5830        // unconvert data redactor tag
5831        var $wrapper = this.utils.buildWrapper(html);
5832        $wrapper.find('*').removeAttr('style');
5833        $wrapper.find('[data-redactor-tag]').each(function(node) {
5834            var $node = $R.dom(node);
5835            $node.removeAttr('data-redactor-tag');
5836
5837            if (this.utils.isEmptyHtml($node.html())) {
5838                $node.html('<br>').unwrap();
5839            }
5840            else if (node.lastChild && node.lastChild.tagName === 'BR') {
5841                $node.unwrap();
5842            }
5843            else {
5844                $node.append('<br>').unwrap();
5845            }
5846        }.bind(this));
5847        html = this.utils.getWrapperHtml($wrapper);
5848        html = html.replace(/<br\s?\/?>$/, '');
5849        html = html.replace(/<br\s?\/?><\/(td|th)>/, '</$1>');
5850
5851        // remove tags
5852        var exceptedTags = this.opts.pasteBlockTags.concat(this.opts.pasteInlineTags);
5853        html = this.removeTagsExcept(html, exceptedTags);
5854
5855        // links & images
5856        html = (this.opts.pasteLinks) ? html : this.removeTags(html, ['a']);
5857        html = (this.opts.pasteImages) ? html : this.removeTags(html, ['img']);
5858
5859        // build wrapper
5860        var $wrapper = this.utils.buildWrapper(html);
5861
5862        // clean attrs
5863        var $elms = $wrapper.find('*');
5864
5865        // remove style
5866        var filterStyle = (this.opts.pasteKeepStyle.length !== 0) ? ',' + this.opts.pasteKeepStyle.join(',') : '';
5867        $elms.not('[data-redactor-style-cache]' + filterStyle).removeAttr('style');
5868
5869        // remove class
5870        var filterClass = (this.opts.pasteKeepClass.length !== 0) ? ',' + this.opts.pasteKeepClass.join(',') : '';
5871        $elms.not('[data-redactor-style-cache], span.redactor-component' + filterClass).removeAttr('class');
5872
5873        // remove attrs
5874        var filterAttrs = (this.opts.pasteKeepAttrs.length !== 0) ? ',' + this.opts.pasteKeepAttrs.join(',') : '';
5875        $elms.not('img, a, span.redactor-component, [data-redactor-style-cache]' + filterAttrs).each(function(node)
5876        {
5877            var attrs = node.attributes;
5878            for (var i = attrs.length - 1; i >= 0; i--) {
5879                if (node.attributes[i].name !== 'class' && node.attributes[i].name !== 'dir') {
5880                    node.removeAttribute(attrs[i].name);
5881                }
5882            }
5883        });
5884
5885        // paste link target
5886        if (this.opts.pasteLinks && this.opts.pasteLinkTarget !== false)
5887        {
5888            $wrapper.find('a').attr('target', this.opts.pasteLinkTarget);
5889        }
5890
5891        // keep style
5892        $wrapper.find('[data-redactor-style-cache]').each(function(node)
5893        {
5894            var style = node.getAttribute('data-redactor-style-cache');
5895            node.setAttribute('style', style);
5896        });
5897
5898        // remove image attributes
5899        var imageattrs = this.opts.imageAttrs;
5900        $wrapper.find('img').each(function(node) {
5901            if (node.attributes.length > 0) {
5902                var attrs = node.attributes;
5903                for (var i = attrs.length - 1; i >= 0; i--) {
5904                    if (imageattrs.indexOf(attrs[i].name) === -1) {
5905                        node.removeAttribute(attrs[i].name);
5906                    }
5907                }
5908            }
5909        });
5910
5911        // remove empty span
5912        $wrapper.find('span').each(function(node)
5913        {
5914            if (node.attributes.length === 0)
5915            {
5916                $R.dom(node).unwrap();
5917            }
5918        });
5919
5920        // remove empty inline
5921        $wrapper.find(this.opts.inlineTags.join(',')).each(function(node)
5922        {
5923            if (node.attributes.length === 0 && this.utils.isEmptyHtml(node.innerHTML))
5924            {
5925                $R.dom(node).unwrap();
5926            }
5927
5928        }.bind(this));
5929
5930        // place ul/ol into li
5931        $wrapper.find('ul, ol').each(function(node)
5932        {
5933            var prev = node.previousSibling;
5934            if (prev && prev.tagName === 'LI')
5935            {
5936                var $li = $R.dom(prev);
5937                $li.find('p').unwrap();
5938                $li.append(node);
5939            }
5940        });
5941
5942        // get wrapper
5943        html = this.utils.getWrapperHtml($wrapper);
5944
5945        // remove paragraphs form lists (google docs bug)
5946        html = html.replace(/<li><p>/gi, '<li>');
5947        html = html.replace(/<\/p><\/li>/gi, '</li>');
5948
5949        // gmail list paste
5950        html = html.replace(/^<li/gi, '<ul><li');
5951        html = html.replace(/<\/li>$/gi, '</li></ul>');
5952
5953        // convert lines to br
5954        if (this.opts.breakline) {
5955            html = html.replace(/\n/g, '<br>');
5956        }
5957
5958        // clean empty p
5959        html = html.replace(/<p>&nbsp;<\/p>/gi, '<p></p>');
5960        html = html.replace(/<p><br\s?\/?><\/p>/gi, '<p></p>');
5961
5962        if (isMsWord)
5963        {
5964            html = html.replace(/<p><\/p>/gi, '');
5965            html = html.replace(/<p>\s<\/p>/gi, '');
5966            html = html.replace(/<td\n/gi, '<td ');
5967        }
5968
5969        // restore components
5970        html = this.restoreComponents(html);
5971
5972        return html;
5973    },
5974    pastePlainText: function(html)
5975    {
5976        html = (this.opts.pasteLinks) ? this.storeLinks(html) : html;
5977        html = (this.opts.pasteImages) ? this.storeImages(html) : html;
5978
5979        html = this.getPlainText(html);
5980        html = this._replaceNlToBr(html);
5981
5982        html = (this.opts.pasteLinks) ? this.restoreLinks(html) : html;
5983        html = (this.opts.pasteImages) ? this.restoreImages(html) : html;
5984
5985        return html;
5986    },
5987    tidy: function(html)
5988    {
5989        return html;
5990    },
5991    paragraphize: function(html)
5992    {
5993        var paragraphize = $R.create('cleaner.paragraphize', this.app);
5994        html = paragraphize.convert(html);
5995        return html;
5996    },
5997    storeComments: function(html, storedComments) {
5998        var comments = html.match(new RegExp('<!--([\\w\\W]*?)-->', 'gi'));
5999        if (comments !== null) {
6000            for (var i = 0; i < comments.length; i++) {
6001                html = html.replace(comments[i], '#####xstarthtmlcommentzz' + i + 'xendhtmlcommentzz#####');
6002                storedComments.push(comments[i]);
6003            }
6004        }
6005
6006        return html;
6007    },
6008    restoreComments: function(html, storedComments) {
6009        for (var i = 0; i < storedComments.length; i++) {
6010            html = html.replace('#####xstarthtmlcommentzz' + i + 'xendhtmlcommentzz#####', storedComments[i]);
6011        }
6012
6013        return html;
6014    },
6015
6016    // get
6017    getFlatText: function(html)
6018    {
6019        var $div = $R.dom('<div>');
6020
6021        if (!html.nodeType && !html.dom)
6022        {
6023            html = html.toString();
6024            html = html.trim();
6025            $div.html(html);
6026        }
6027        else
6028        {
6029            $div.append(html);
6030        }
6031
6032        html = $div.get().textContent || $div.get().innerText || '';
6033
6034        return (html === undefined) ? '' : html;
6035    },
6036	getPlainText: function(html)
6037	{
6038		html = html.replace(/<!--[\s\S]*?-->/gi, '');
6039		html = html.replace(/<style[\s\S]*?style>/gi, '');
6040        html = html.replace(/<p><\/p>/g, '');
6041		html = html.replace(/<\/div>|<\/li>|<\/td>/gi, '\n');
6042		html = html.replace(/<\/p>/gi, '\n\n');
6043		html = html.replace(/<\/H[1-6]>/gi, '\n\n');
6044
6045		var tmp = document.createElement('div');
6046		tmp.innerHTML = html;
6047
6048		html = tmp.textContent || tmp.innerText;
6049
6050		return html.trim();
6051	},
6052
6053    // replace
6054    replaceTags: function(html, tags)
6055    {
6056        if (tags)
6057        {
6058            var self = this;
6059            var keys = Object.keys(tags);
6060            var $wrapper = this.utils.buildWrapper(html);
6061            $wrapper.find(keys.join(',')).each(function(node)
6062            {
6063                self.utils.replaceToTag(node, tags[node.tagName.toLowerCase()]);
6064            });
6065
6066            html = this.utils.getWrapperHtml($wrapper);
6067        }
6068
6069        return html;
6070    },
6071    replaceNbspToSpaces: function(html)
6072    {
6073        return html.replace('&nbsp;', ' ');
6074    },
6075    replaceBlocksToBr: function(html)
6076    {
6077        html = html.replace(/<div[^>]*><\/div>/gi, '');
6078        html = html.replace(/<td[^>
6078]*><\/td>/gi, '');
6079        html = html.replace(/<\/div>|<\/li>|<\/td>|<\/p>|<\/H[1-6]>/gi, '<br>');
6080
6081        return html;
6082    },
6083
6084    // clean
6085    cleanNewLines: function(html)
6086    {
6087        return html.replace(/\r?\n/g, "");
6088    },
6089    cleanSpacesInPre: function(html)
6090    {
6091        return html.replace('&nbsp;&nbsp;&nbsp;&nbsp;', '    ');
6092    },
6093
6094    // remove
6095    removeInvisibleSpaces: function(html)
6096    {
6097        html = this.utils.removeInvisibleChars(html);
6098        html = html.replace(/&#65279;/gi, '');
6099
6100        return html;
6101    },
6102    removeNl: function(html)
6103    {
6104        html = html.replace(/\n/g, " ");
6105        html = html.replace(/\s+/g, "\s");
6106
6107        return html;
6108    },
6109    removeBrAtEnd: function(html)
6110    {
6111        html = html.replace(/<br\s?\/?>$/gi, ' ');
6112        html = html.replace(/<br\s?\/?><li/gi, '<li');
6113
6114        return html;
6115    },
6116    removeTags: function(input, denied)
6117    {
6118        var re = (denied) ? /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi : /(<([^>]+)>)/gi;
6119        var replacer = (!denied) ? '' : function ($0, $1)
6120        {
6121            return denied.indexOf($1.toLowerCase()) === -1 ? $0 : '';
6122        };
6123
6124        return input.replace(re, replacer);
6125    },
6126    removeTagsExcept: function(input, except)
6127    {
6128        if (except === undefined) return input.replace(/(<([^>]+)>)/gi, '');
6129
6130        var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi;
6131        return input.replace(tags, function($0, $1)
6132        {
6133            return except.indexOf($1.toLowerCase()) === -1 ? '' : $0;
6134        });
6135    },
6136    removeComments: function(html)
6137    {
6138        return html.replace(this.reComments, '');
6139    },
6140    removeServiceTagsAndAttrs: function(html, removeMarkers)
6141    {
6142        var $wrapper = this.utils.buildWrapper(html);
6143        var self = this;
6144        if (removeMarkers !== false)
6145        {
6146            $wrapper.find('.redactor-selection-marker').each(function(node)
6147            {
6148                var $el = $R.dom(node);
6149                var text = self.utils.removeInvisibleChars($el.text());
6150
6151                return (text === '') ? $el.remove() : $el.unwrap();
6152            });
6153        }
6154
6155        $wrapper.find('[data-redactor-style-cache]').removeAttr('data-redactor-style-cache');
6156
6157        return this.utils.getWrapperHtml($wrapper);
6158    },
6159    removeSpanWithoutAttributes: function(html)
6160    {
6161        var $wrapper = this.utils.buildWrapper(html);
6162        $wrapper.find('span').removeAttr('data-redactor-span data-redactor-style-cache').each(function(node)
6163        {
6164            if (node.attributes.length === 0) $R.dom(node).unwrap();
6165        });
6166
6167        return this.utils.getWrapperHtml($wrapper);
6168    },
6169    removeFirstBlockBreaklineInHtml: function(html)
6170    {
6171        return html.replace(new RegExp('</li><br\\s?/?>', 'gi'), '</li>');
6172    },
6173    removeEmptyAttributes: function(html, attrs)
6174    {
6175        var $wrapper = this.utils.buildWrapper(html);
6176        for (var i = 0; i < attrs.length; i++)
6177        {
6178            $wrapper.find('[' + attrs[i] + '=""]').removeAttr(attrs[i]);
6179        }
6180
6181        return this.utils.getWrapperHtml($wrapper);
6182    },
6183
6184    // encode / decode
6185    encodeHtml: function(html)
6186    {
6187        html = html.replace(/<br\s?\/?>/g, "\n");
6188        html = html.replace(/&nbsp;/g, ' ');
6189        html = html.replace(/”/g, '"');
6190        html = html.replace(/“/g, '"');
6191        html = html.replace(/‘/g, '\'');
6192        html = html.replace(/’/g, '\'');
6193        html = this.encodeEntities(html);
6194        html = html.replace(/\$/g, '&#36;');
6195
6196        if (this.opts.preSpaces)
6197        {
6198            html = html.replace(/\t/g, new Array(this.opts.preSpaces + 1).join(' '));
6199        }
6200
6201        return html;
6202    },
6203
6204    encodeCode: function(html) {
6205
6206        html = this.encodeAttrSings(html);
6207
6208        // replace all tags
6209        html = html.replace(/<\s/gi, '&lt; ');
6210        html = html.replace(/<([^>]+)</gi, '&lt;$1<');
6211        html = html.replace(/<(.*?)>/gi, 'xtagstartz$1xtagendz');
6212
6213        // revert pre / code
6214        html = html.replace(/xtagstartzpre(.*?)xtagendz/g, '<pre$1>');
6215        html = html.replace(/xtagstartzcode(.*?)xtagendz/g, '<code$1>');
6216        html = html.replace(/xtagstartz\/codextagendz/g, '</code>');
6217        html = html.replace(/xtagstartz\/prextagendz/g, '</pre>');
6218
6219        // encode
6220        html = this._encodeCode(html);
6221
6222        // revert all tags
6223        html = html.replace(/xtagstartz([\w\W]*?)xtagendz/g, '<$1>');
6224        html = html.replace(/xtagstartz\/(.*?)xtagendz/g, '</$1>');
6225
6226        html = this.decodeAttrSings(html);
6227
6228        return html;
6229    },
6230    _encodeCode: function(html) {
6231        var $wrapper = this.utils.buildWrapper(html);
6232        $wrapper.find('pre code, pre, code').each(this._encodeNode.bind(this));
6233
6234        return this.utils.getWrapperHtml($wrapper);
6235    },
6236    _encodeNode: function(node) {
6237        var first = node.firstChild;
6238        var html = node.innerHTML;
6239        if (node.tagName === 'PRE' && first && first.tagName === 'CODE') {
6240            return;
6241        }
6242
6243        html = html.replace(/xtagstartz/g, '<');
6244        html = html.replace(/xtagendz/g, '>');
6245
6246        var encoded = this.decodeEntities(html);
6247        node.textContent = this._encodeNodeHtml(encoded);
6248    },
6249    _encodeNodeHtml: function(html) {
6250        html = html.replace(/&nbsp;/g, ' ').replace(/<br\s?\/?>/g, '\n');
6251        html = (this.opts.preSpaces) ? html.replace(/\t/g, new Array(this.opts.preSpaces + 1).join(' ')) : html;
6252
6253        return html;
6254    },
6255
6256    encodeAttrSings: function(html) {
6257        var matches = html.match(/="(.*?)"/g);
6258        if (matches !== null) {
6259            for (var i = 0; i < matches.length; i++) {
6260                if (matches[i].search(/^"</) !== -1 || matches[i].search(/>"$/) !== -1) {
6261                    continue;
6262                }
6263
6264
6265                var str = matches[i].replace('>', 'xmoresignz');
6266                str = str.replace('<', 'xlesssignz');
6267                html = html.replace(matches[i], str);
6268            }
6269        }
6270
6271        return html;
6272    },
6273    encodeEntities: function(str)
6274    {
6275        str = this.decodeEntities(str);
6276        str = str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
6277
6278        return str;
6279    },
6280    encodePhpCode: function(html)
6281    {
6282        html = html.replace('<?php', '&lt;?php');
6283        html = html.replace('<?', '&lt;?');
6284        html = html.replace('?>', '?&gt;');
6285
6286        return html;
6287    },
6288    decodeAttrSings: function(html) {
6289        html = html.replace(/xmoresignz/gi, '>');
6290        html = html.replace(/xlesssignz/gi, '<');
6291
6292        return html;
6293    },
6294    decodeEntities: function(str)
6295    {
6296        return String(str).replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&amp;/g, '&');
6297    },
6298
6299    // store / restore
6300    storeComponents: function(html)
6301    {
6302        var matched = this.utils.getElementsFromHtml(html, 'figure', 'table');
6303
6304        return this._storeMatched(html, matched, 'Components', 'figure');
6305    },
6306    restoreComponents: function(html)
6307    {
6308        return this._restoreMatched(html, 'Components', 'figure');
6309    },
6310    storeLinks: function(html)
6311    {
6312        var matched = this.utils.getElementsFromHtml(html, 'a');
6313
6314        return this._storeMatched(html, matched, 'Links', 'a');
6315    },
6316    storeImages: function(html)
6317    {
6318        var matched = this.utils.getElementsFromHtml(html, 'img');
6319
6320        return this._storeMatched(html, matched, 'Images', 'img');
6321    },
6322    restoreLinks: function(html)
6323    {
6324        return this._restoreMatched(html, 'Links', 'a');
6325    },
6326    restoreImages: function(html)
6327    {
6328        return this._restoreMatched(html, 'Images', 'img');
6329    },
6330
6331    // PRIVATE
6332
6333    // clean
6334    _cleanWrapped: function(html)
6335    {
6336        html = html.replace(new RegExp('<p><figure([\\w\\W]*?)</figure></p>', 'gi'), '<figure$1</figure>');
6337
6338        return html;
6339    },
6340    _cleanGDocs: function(html)
6341    {
6342        // remove google docs markers
6343        html = html.replace(/<b\sid="internal-source-marker(.*?)">([\w\W]*?)<\/b>/gi, "$2");
6344        html = html.replace(/<b(.*?)id="docs-internal-guid(.*?)">([\w\W]*?)<\/b>/gi, "$3");
6345
6346        html = html.replace(/<span[^>]*(font-style:\s?italic;\s?font-weight:\s?bold|font-weight:\s?bold;\s?font-style:\s?italic)[^>]*>([\w\W]*?)<\/span>/gi, '<b><i>$2</i></b>');
6347        html = html.replace(/<span[^>]*(font-style:\s?italic;\s?font-weight:\s?600|font-weight:\s?600;\s?font-style:\s?italic)[^>]*>([\w\W]*?)<\/span>/gi, '<b><i>$2</i></b>');
6348        html = html.replace(/<span[^>]*(font-style:\s?italic;\s?font-weight:\s?700|font-weight:\s?700;\s?font-style:\s?italic)[^>]*>([\w\W]*?)<\/span>/gi, '<b><i>$2</i></b>');
6349        html = html.replace(/<span[^>]*font-style:\s?italic[^>]*>([\w\W]*?)<\/span>/gi, '<i>$1</i>');
6350        html = html.replace(/<span[^>
6350]*font-weight:\s?bold[^>]*>([\w\W]*?)<\/span>/gi, '<b>$1</b>');
6351        html = html.replace(/<span[^>]*font-weight:\s?700[^>]*>([\w\W]*?)<\/span>/gi, '<b>$1</b>');
6352        html = html.replace(/<span[^>]*font-weight:\s?600[^>]*>([\w\W]*?)<\/span>/gi, '<b>$1</b>');
6353
6354        return html;
6355    },
6356    _cleanMsWord: function(html)
6357    {
6358        html = html.replace(/<!--[\s\S]+?-->/gi, '');
6359        html = html.replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi, '');
6360        html = html.replace(/<(\/?)s>/gi, "<$1strike>");
6361        html = html.replace(/&nbsp;/gi, ' ');
6362        html = html.replace(/<span\s+style\s*=\s*"\s*mso-spacerun\s*:\s*yes\s*;?\s*"\s*>([\s\u00a0]*)<\/span>/gi, function(str, spaces) {
6363            return (spaces.length > 0) ? spaces.replace(/./, " ").slice(Math.floor(spaces.length/2)).split("").join("\u00a0") : '';
6364        });
6365
6366        // build wrapper
6367        var $wrapper = this.utils.buildWrapper(html);
6368
6369        // footnote fix
6370        $wrapper.find('.MsoFootnoteText').each(function(node) {
6371            var $node = $R.dom(node);
6372            var $parent = $node.parent();
6373            if ($parent.length !== 0 && $parent.attr('style').search(/mso-element:footnote/) !== -1) {
6374                $node.find('a').attr('id', '_' + $parent.attr('id'));
6375            }
6376
6377        });
6378
6379        $wrapper.find('.MsoFootnoteReference').each(function(node) {
6380            var $node = $R.dom(node);
6381            var $parent = $node.parent();
6382            if ($parent.length !== 0 && $parent.get().tagName === 'A') {
6383                $parent.attr('id', $parent.attr('name'));
6384            }
6385
6386        });
6387
6388        // build lists
6389        $wrapper.find('p').each(function(node)
6390        {
6391            var $node = $R.dom(node);
6392            var str = $node.attr('style');
6393            var matches = /mso-list:\w+ \w+([0-9]+)/.exec(str);
6394            if (matches)
6395            {
6396                $node.attr('data-listLevel',  parseInt(matches[1], 10));
6397            }
6398        });
6399
6400        // parse Lists
6401        this._parseWordLists($wrapper);
6402
6403        $wrapper.find('[align]').removeAttr('align');
6404        $wrapper.find('[name]').removeAttr('name');
6405        $wrapper.find('span').each(function(node)
6406        {
6407            var $node = $R.dom(node);
6408            var str = $node.attr('style');
6409            var matches = /mso-list:Ignore/.exec(str);
6410
6411            if (matches) $node.remove();
6412            else $node.unwrap();
6413
6414        });
6415        $wrapper.find('[style]').removeAttr('style');
6416        $wrapper.find("[class^='Mso']").removeAttr('class');
6417        $wrapper.find('a').filter(function(node) { return !node.hasAttribute('href'); }).unwrap();
6418
6419        // get wrapper
6420        html = this.utils.getWrapperHtml($wrapper);
6421        html = html.replace(/<p[^>]*><\/p>/gi, '');
6422        html = html.replace(/<li>·/gi, '<li>');
6423        html = html.trim();
6424
6425        // remove spaces between
6426        html = html.replace(/\/(p|ul|ol|h1|h2|h3|h4|h5|h6|blockquote)>\s+<(p|ul|ol|h1|h2|h3|h4|h5|h6|blockquote)/gi, '/$1>\n<$2');
6427
6428        var result = '';
6429        var lines = html.split(/\n/);
6430        for (var i = 0; i < lines.length; i++)
6431        {
6432            var space = (lines[i] !== '' && lines[i].search(/>$/) === -1) ? ' ' : '\n';
6433
6434            result += lines[i] + space;
6435        }
6436
6437        return result;
6438    },
6439    _parseWordLists: function($wrapper)
6440    {
6441        var lastLevel = 0;
6442        var $item = null;
6443        var $list = null;
6444        var $listChild = null;
6445
6446        $wrapper.find('p').each(function(node) {
6447            var $node = $R.dom(node);
6448            var level = $node.attr('data-listLevel');
6449            if (level === null && $node.hasClass('MsoListParagraphCxSpMiddle')) {
6450                level = 1;
6451            }
6452
6453            if (level !== null) {
6454                var txt = $node.text();
6455                var listTag = (/^\s*\w+\./.test(txt)) ? '<ol></ol>' : '<ul></ul>';
6456
6457                // new parent list
6458                if ($node.hasClass('MsoListParagraphCxSpFirst') || $node.hasClass('MsoNormal')) {
6459                    $list = $R.dom(listTag);
6460                    $node.before($list);
6461                }
6462                // new child list
6463                else if (level > lastLevel && lastLevel !== 0) {
6464                    $listChild = $R.dom(listTag);
6465                    $item.append($listChild);
6466                    $list = $listChild;
6467                }
6468                // level up
6469                if (level < lastLevel) {
6470                    var len = lastLevel - level + 1;
6471                    for (var i = 0; i < len; i++) {
6472                        $list = $list.parent();
6473                    }
6474                }
6475
6476                // create item
6477                $node.find('span').first().unwrap();
6478                $item = $R.dom('<li>' + $node.html().trim() + '</li>');
6479                if ($list === null) {
6480                    $node.before(listTag);
6481                    $list = $node.prev();
6482                }
6483
6484                // append
6485                $list.append($item);
6486                $node.remove();
6487
6488                lastLevel = level;
6489            }
6490            else {
6491                $list = null;
6492                lastLevel = 0;
6493            }
6494        });
6495    },
6496
6497    // is
6498    _isSpacedEmpty: function(html)
6499    {
6500        return (html.search(this.reSpacedEmpty) !== -1);
6501    },
6502    _isParagraphEmpty: function(html)
6503    {
6504        return (html.search(/^<p><\/p>$/i) !== -1);
6505    },
6506    _isHtmlMsWord: function(html)
6507    {
6508        return html.match(/class="?Mso|style="[^"]*\bmso-|style='[^'']*\bmso-|w:WordDocument/i);
6509    },
6510
6511    // set
6512    _setSpanAttr: function(html)
6513    {
6514        var $wrapper = this.utils.buildWrapper(html);
6515        $wrapper.find('span').attr('data-redactor-span', true);
6516
6517        return this.utils.getWrapperHtml($wrapper);
6518    },
6519    _setStyleCache: function(html)
6520    {
6521        var $wrapper = this.utils.buildWrapper(html);
6522        $wrapper.find('[style]').each(function(node)
6523        {
6524            var $el = $R.dom(node);
6525            $el.attr('data-redactor-style-cache', $el.attr('style'));
6526        });
6527
6528        return this.utils.getWrapperHtml($wrapper);
6529    },
6530    _setPreClass: function(html)
6531    {
6532        var $wrapper = this.utils.buildWrapper(html);
6533        $wrapper.find('pre').addClass(this.opts.preClass);
6534
6535        return this.utils.getWrapperHtml($wrapper);
6536    },
6537    _setLinkNofollow: function(html)
6538    {
6539        var $wrapper = this.utils.buildWrapper(html);
6540        $wrapper.find('a').attr('rel', 'nofollow');
6541
6542        return this.utils.getWrapperHtml($wrapper);
6543    },
6544
6545    // replace
6546    _replaceScriptTag: function(html)
6547    {
6548        return html.replace(this.reScriptTag, '<script class="redactor-script-tag" $1>$2</script>');
6549    },
6550    _unreplaceScriptTag: function(html)
6551    {
6552        return html.replace(/<script class="redactor-script-tag"(.*?[^>]?)>([\w\W]*?)<\/script>/gi, '<script$1>$2</script>');
6553    },
6554	_replaceNlToBr: function(html)
6555	{
6556		return html.replace(/\n/g, '<br />');
6557	},
6558
6559    // remove
6560    _removeScriptTag: function(html)
6561    {
6562        return html.replace(this.reScriptTag, '');
6563    },
6564
6565    // private
6566    _storeMatched: function(html, matched, stored, name)
6567    {
6568        this['stored' + stored] = [];
6569        if (matched)
6570        {
6571            for (var i = 0; i < matched.length; i++)
6572            {
6573                this['stored' + stored][i] = matched[i];
6574                html = html.replace(matched[i], '####' + name + i + '####');
6575            }
6576        }
6577
6578        return html;
6579    },
6580    _restoreMatched: function(html, stored, name)
6581    {
6582        if (this['stored' + stored])
6583        {
6584            for (var i = 0; i < this['stored' + stored].length; i++)
6585            {
6586                html = html.replace('####' + name + i + '####', this['stored' + stored][i]);
6587            }
6588        }
6589
6590        return html;
6591    }
6592});
6593$R.add('class', 'cleaner.figure', {
6594    init: function(app)
6595    {
6596        this.app = app;
6597        this.opts = app.opts;
6598        this.utils = app.utils;
6599        this.detector = app.detector;
6600    },
6601    // public
6602    convert: function(html, rules)
6603    {
6604        var $wrapper = this.utils.buildWrapper(html);
6605
6606        // convert
6607        $wrapper.find('img').each(this._convertImage.bind(this));
6608        $wrapper.find('hr').each(this._convertLine.bind(this));
6609        $wrapper.find('iframe').each(this._convertIframe.bind(this));
6610        $wrapper.find('table').each(this._convertTable.bind(this));
6611        $wrapper.find('form').each(this._convertForm.bind(this));
6612        $wrapper.find('figure pre').each(this._convertCode.bind(this));
6613
6614        // variables
6615        $wrapper.find('[data-redactor-type=variable]').addClass('redactor-component');
6616
6617        // widgets
6618        $wrapper.find('figure').not('.redactor-component, .redactor-figure-code').each(this._convertWidget.bind(this));
6619
6620        // contenteditable
6621        $wrapper.find('figure pre').each(this._setContenteditableCode.bind(this));
6622        $wrapper.find('.redactor-component, .non-editable').attr('contenteditable', false);
6623
6624        if (this.detector.isIe())
6625        {
6626            $wrapper.find('[data-redactor-type=table]').removeAttr('contenteditable');
6627        }
6628
6629        $wrapper.find('figcaption, td, th').attr('contenteditable', true);
6630        $wrapper.find('.redactor-component, figcaption').attr('tabindex', '-1');
6631
6632        // extra rules
6633        this._acceptExtraRules($wrapper, rules);
6634
6635        return this.utils.getWrapperHtml($wrapper);
6636    },
6637    unconvert: function(html, rules)
6638    {
6639        html = html.replace(/<\/([^>]+)><div data-redactor-tag/g, '</$1>\n<div data-redactor-tag');
6640        html = html.replace(/<\/([^>]+)><p/g, '</$1>\n<p');
6641
6642        var $wrapper = this.utils.buildWrapper(html);
6643
6644        // contenteditable
6645        $wrapper.find('th, td, figcaption, figure, pre, code, .redactor-component').removeAttr('contenteditable tabindex');
6646
6647        // remove class
6648        $wrapper.find('figure').removeClass('redactor-component redactor-component-active redactor-uploaded-figure');
6649
6650        // unconvert
6651        $wrapper.find('[data-redactor-type=variable]').removeClass('redactor-component redactor-component-active');
6652        $wrapper.find('figure[data-redactor-type=line]').unwrap();
6653        $wrapper.find('figure[data-redactor-type=widget]').each(this._unconvertWidget.bind(this));
6654        $wrapper.find('figure[data-redactor-type=form]').each(this._unconvertForm.bind(this));
6655        $wrapper.find('figure[data-redactor-type=table]').each(this._unconvertTable.bind(this));
6656        $wrapper.find('figure[data-redactor-type=image]').removeAttr('rel').each(this._unconvertImages.bind(this));
6657
6658        $wrapper.find('img').removeAttr('data-redactor-type').removeClass('redactor-component');
6659        $wrapper.find('.non-editable').removeAttr('contenteditable');
6660
6661        // remove types
6662        $wrapper.find('figure').each(this._removeTypes.bind(this));
6663
6664        // remove caret
6665        $wrapper.find('span.redactor-component-caret').remove();
6666
6667        // break div
6668        $wrapper = this._unconvertBreakTag($wrapper);
6669
6670        // extra rules
6671        this._acceptExtraRules($wrapper, rules);
6672
6673        html = this.utils.getWrapperHtml($wrapper);
6674        html = html.replace(/<br\s?\/?>$/, '');
6675        html = html.replace(/<br\s?\/?><\/(td|th)>/, '</$1>');
6676
6677        return html;
6678    },
6679
6680    // private
6681    _convertImage: function(node)
6682    {
6683        var $node = $R.dom(node);
6684        if (this._isNonEditable($node)) return;
6685
6686        // set id
6687        if (this.opts.imageObserve && !$node.attr('data-image'))
6688        {
6689            $node.attr('data-image', this.utils.getRandomId());
6690        }
6691
6692        var $link = $node.closest('a');
6693        var $figure = $node.closest('figure');
6694        var isImage = ($figure.children().not('a, img, br, figcaption').length === 0);
6695        if (!isImage) return;
6696
6697        if ($figure.length === 0)
6698        {
6699            var $parent = ($link.length !== 0) ? $link.closest('p') : $node.closest('p');
6700            if (this.opts.imageFigure === false && $parent.length !== 0)
6701            {
6702                var $el = this.utils.replaceToTag($parent, 'figure');
6703                $figure = $el;
6704                $figure.addClass('redactor-replace-figure');
6705            }
6706            else
6707            {
6708                if ($parent.length !== 0) {
6709                    $parent.unwrap();
6710                }
6711
6712                $figure = ($link.length !== 0) ? $link.wrap('<figure>') : $node.wrap('<figure>');
6713            }
6714        }
6715        else
6716        {
6717            if ($figure.hasClass('redactor-uploaded-figure'))
6718            {
6719                $figure.removeClass('redactor-uploaded-figure');
6720            }
6721            else
6722            {
6723                $figure.addClass('redactor-keep-figure');
6724            }
6725        }
6726
6727        this._setFigure($figure, 'image');
6728    },
6729    _convertTable: function(node)
6730    {
6731        if (this._isNonEditable(node)) return;
6732
6733        var $figure = this._wrapFigure(node);
6734        this._setFigure($figure, 'table');
6735    },
6736    _convertLine: function(node)
6737    {
6738        if (this._isNonEditable(node)) return;
6739
6740        var $figure = this._wrapFigure(node);
6741        this._setFigure($figure, 'line');
6742    },
6743    _convertForm: function(node)
6744    {
6745        if (this._isNonEditable(node)) return;
6746
6747        var $figure = this.utils.replaceToTag(node, 'figure');
6748        this._setFigure($figure, 'form');
6749    },
6750    _convertIframe: function(node)
6751    {
6752        if (this._isNonEditable(node)) return;
6753
6754        var $node = $R.dom(node);
6755        if ($node.closest('.redactor-component').length !== 0) return;
6756
6757        var src = node.getAttribute('src');
6758        var isVideo = (src && (src.match(this.opts.regex.youtube) || src.match(this.opts.regex.vimeo)));
6759        var $figure = this._wrapFigure(node);
6760
6761        if (isVideo)
6762        {
6763            this._setFigure($figure, 'video');
6764        }
6765    },
6766    _convertCode: function(node)
6767    {
6768        if (this._isNonEditable(node)) return;
6769
6770        var $figure = this._wrapFigure(node);
6771        this._setFigure($figure, 'code');
6772    },
6773    _convertWidget: function(node)
6774    {
6775        if (this._isNonEditable(node)) return;
6776
6777        var $node = $R.dom(node);
6778        $node.addClass('redactor-component');
6779        $node.attr('data-redactor-type', 'widget');
6780        $node.attr('data-widget-code', encodeURI(node.innerHTML.trim()));
6781    },
6782
6783    // unconvert
6784    _unconvertBreakTag: function($wrapper) {
6785        $wrapper.find('[data-redactor-tag]').each(function(node) {
6786            var $node = $R.dom(node);
6787            $node.removeAttr('data-redactor-tag');
6788            if (node.attributes.length !== 0) {
6789                if (node.lastChild && node.lastChild.tagName === 'BR') $R.dom(node.lastChild).remove();
6790                return;
6791            }
6792
6793            if (node.lastChild && node.lastChild.tagName === 'BR') {
6794                $node.unwrap();
6795            }
6796            else {
6797                var $next = $node.nextElement();
6798                if ($next.length !== 0 && $next.attr('data-redactor-tag')) {
6799                    node.appendChild(document.createElement('br'));
6800                }
6801
6802                $node.unwrap();
6803            }
6804        }.bind(this));
6805        return $wrapper;
6806    },
6807    _unconvertForm: function(node)
6808    {
6809        this.utils.replaceToTag(node, 'form');
6810    },
6811    _unconvertTable: function(node)
6812    {
6813        var $node = $R.dom(node);
6814        $node.unwrap();
6815    },
6816    _unconvertWidget: function(node)
6817    {
6818        var $node = $R.dom(node);
6819        $node.html(decodeURI($node.attr('data-widget-code')));
6820        $node.removeAttr('data-widget-code');
6821    },
6822    _unconvertImages: function(node)
6823    {
6824        var $node = $R.dom(node);
6825        $node.removeClass('redactor-component');
6826
6827        var isList = ($node.closest('li').length !== 0);
6828        var isTable = ($node.closest('table').length !== 0);
6829        var hasFigcaption = ($node.find('figcaption').length !== 0);
6830
6831        var style = $node.attr('style');
6832        var hasStyle = !(style === null || style === '');
6833        var hasClass = ($node.attr('class') !== '');
6834
6835        if (isList || (isTable && !hasFigcaption && !hasStyle && !hasClass))
6836        {
6837            $node.unwrap();
6838        }
6839    },
6840    _removeTypes: function(node)
6841    {
6842        var $node = $R.dom(node);
6843        var type = $node.attr('data-redactor-type');
6844        var r
6844emoved = ['image', 'widget', 'line', 'video', 'code', 'form', 'table'];
6845        if (type && removed.indexOf(type) !== -1)
6846        {
6847            $node.removeAttr('data-redactor-type');
6848        }
6849
6850        // keep figure
6851        if ($node.hasClass('redactor-keep-figure'))
6852        {
6853            $node.removeClass('redactor-keep-figure');
6854        }
6855
6856        // unwrap figure
6857        else if (type === 'image' && this.opts.imageFigure === false)
6858        {
6859            var hasFigcaption = ($node.find('figcaption').length !== 0);
6860            if (!hasFigcaption)
6861            {
6862                // replace
6863                if ($node.hasClass('redactor-replace-figure'))
6864                {
6865                    $node.removeClass('redactor-replace-figure');
6866                    this.utils.replaceToTag($node, 'p');
6867                }
6868                else
6869                {
6870                    this.utils.replaceToTag($node, 'p');
6871                }
6872            }
6873        }
6874
6875        $node.removeClass('redactor-replace-figure');
6876    },
6877
6878    // wrap
6879    _wrapFigure: function(node)
6880    {
6881        var $node = $R.dom(node);
6882        var $figure = $node.closest('figure');
6883
6884        return ($figure.length === 0) ? $node.wrap('<figure>') : $figure;
6885    },
6886
6887    // set
6888    _setFigure: function($figure, type)
6889    {
6890        $figure.addClass('redactor-component');
6891        $figure.attr('data-redactor-type', type);
6892    },
6893    _setContenteditableCode: function(node)
6894    {
6895        if (this._isNonEditable(node)) return;
6896
6897        var $node = $R.dom(node);
6898        var $code = $node.children('code').first();
6899
6900        var $el = ($code.length !== 0) ? $code : $node;
6901        $el.attr('contenteditable', true).attr('tabindex', '-1');
6902    },
6903
6904    // utils
6905    _acceptExtraRules: function($wrapper, rules)
6906    {
6907        for (var key in rules)
6908        {
6909            if (typeof rules[key] === 'function')
6910            {
6911                rules[key]($wrapper);
6912            }
6913        }
6914    },
6915    _isNonEditable: function(node)
6916    {
6917        return ($R.dom(node).closest('.non-editable').length !== 0);
6918    }
6919});
6920$R.add('class', 'cleaner.paragraphize', {
6921    init: function(app) {
6922        this.app = app;
6923        this.opts = app.opts;
6924        this.utils = app.utils;
6925        this.cleaner = app.cleaner;
6926        this.element = app.element;
6927
6928        // local
6929        this.stored = [];
6930        this.remStart = '#####replace';
6931        this.remEnd = '#####';
6932        this.paragraphizeTags = ['table', 'div', 'pre', 'form', 'ul', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'dl', 'blockquote', 'figcaption',
6933                'address', 'section', 'header', 'footer', 'aside', 'article', 'object', 'style', 'script', 'iframe', 'select', 'input', 'textarea',
6934                'button', 'option', 'map', 'area', 'math', 'hr', 'fieldset', 'legend', 'hgroup', 'nav', 'figure', 'details', 'menu', 'summary', 'p'];
6935    },
6936    // public
6937    convert: function(html) {
6938
6939        var value = this._isConverted(html);
6940
6941        value = (value === true) ? this._convert(html) : value;
6942        value = this._convertTable(value);
6943
6944        return value;
6945    },
6946
6947    // private
6948    _convert: function(html, tablemarkup) {
6949        // build markup tag
6950        var tag = (this.opts.breakline || tablemarkup) ? 'sdivtag' : this.opts.markup;
6951        var attr = (tablemarkup) ? 'tbr' : 'br';
6952
6953        // store
6954        html = this._storeTags(html);
6955
6956        var storedComments = [];
6957        html = this.cleaner.storeComments(html, storedComments);
6958
6959        // trim
6960        html = html.trim();
6961        html = this._trimLinks(html);
6962
6963        // trim new line in inline
6964        var inlines = this.opts.inlineTags.join('|');
6965        html = html.replace(new RegExp('<(' + inlines + ')(.*?[^>]?)>\n</(' + inlines + ')>', 'gi'), '<$1$2></$3>');
6966
6967        // replace new lines
6968        html = html.replace(/xparagraphmarkerz(?:\r\n|\r|\n)$/g, '');
6969        html = html.replace(/xparagraphmarkerz$/g, '');
6970        html = html.replace(/xparagraphmarkerz(?:\r\n|\r|\n)/g, '\n');
6971        html = html.replace(/xparagraphmarkerz/g, '\n');
6972
6973        if (this.opts.breakline) {
6974            html = html.replace(/<br\s?\/?>(?:\r\n|\r|\n)/gi, 'xbreakmarkerz\n');
6975            html = html.replace(/<br\s?\/?>/gi, 'xbreakmarkerz\n');
6976            html = html.replace(/xbreakmarkerz\n<\//gi, 'xbreakmarkerz</');
6977
6978        }
6979        else {
6980            html = html.replace(/[\n]+/g, "\n");
6981        }
6982
6983        // wrap to tag
6984        var str = '';
6985        var arr = html.split("\n");
6986        for (var i = 0; i < arr.length; i++) {
6987            str += '<' + tag + '>' + arr[i] + '</' + tag + '>\n';
6988        }
6989
6990        html = str.replace(/\n$/, '');
6991
6992
6993        // clean
6994        html = html.replace(new RegExp('<' + tag + '>\\s+#####', 'gi'), '#####');
6995        html = html.replace(new RegExp('<' + tag + '>#####', 'gi'), '#####');
6996        html = html.replace(new RegExp('#####</' + tag + '>', 'gi'), '#####');
6997
6998        // replace marker
6999        html = (this.opts.breakline) ? html.replace(/xbreakmarkerz/gi, "<br>") : html;
7000
7001        // restore
7002        html = this._restoreTags(html);
7003        html = this.cleaner.restoreComments(html, storedComments);
7004
7005        // remove empty
7006        if (this.opts.breakline) {
7007            html = html.replace(new RegExp('<' + tag + '></' + tag + '>', 'gi'), '<' + tag + '><br></' + tag + '>');
7008        }
7009        else {
7010            //html = html.replace(new RegExp('<' + tag + '><br\\s?/?></' + tag + '>', 'gi'), '');
7011            //html = html.replace(new RegExp('<' + tag + '></' + tag + '>', 'gi'), '');
7012        }
7013
7014        // clean restored
7015        html = html.replace(new RegExp('<sdivtag>', 'gi'), '<div data-redactor-tag="' + attr + '">');
7016        html = html.replace(new RegExp('sdivtag', 'gi'), 'div');
7017        html = html.replace(/<\/([^>]+)><div data-redactor-tag/g, '</$1>\n<div data-redactor-tag');
7018
7019        return html;
7020    },
7021    _convertTable: function(html) {
7022        var $wrapper = this.utils.buildWrapper(html);
7023        $wrapper.find('td, th').each(this._convertCell.bind(this));
7024        html = this.utils.getWrapperHtml($wrapper);
7025
7026        return html;
7027    },
7028    _convertCell: function(node) {
7029        var $node = $R.dom(node);
7030        this.stored = [];
7031        var code = this._convert($node.html(), true);
7032        $node.html(code);
7033    },
7034    _storeTags: function(html) {
7035        var self = this;
7036        var $wrapper = this.utils.buildWrapper(html);
7037        $wrapper.find(this.paragraphizeTags.join(', ')).each(function(node, i) {
7038            var replacement = document.createTextNode(self.remStart + i + self.remEnd + 'xparagraphmarkerz'); //  + "\n"
7039            self.stored.push(node.outerHTML);
7040            node.parentNode.replaceChild(replacement, node);
7041        });
7042
7043        return this.utils.getWrapperHtml($wrapper);
7044    },
7045    _restoreTags: function(html) {
7046        for (var i = 0; i < this.stored.length; i++) {
7047            this.stored[i] = this.stored[i].replace(/\$/g, '&#36;');
7048            html = html.replace(this.remStart + i + this.remEnd, this.stored[i]);
7049        }
7050
7051        return html;
7052    },
7053    _trimLinks: function(html) {
7054        var $wrapper = this.utils.buildWrapper(html);
7055        $wrapper.find('a').each(this._trimLink.bind(this));
7056        html = this.utils.getWrapperHtml($wrapper);
7057
7058        return html;
7059    },
7060    _trimLink: function(node) {
7061        var $node = $R.dom(node);
7062        $node.html($node.html().trim());
7063    },
7064    _isConverted: function(html) {
7065        if (this._isDisabled(html)) return html;
7066        else if (this._isEmptyHtml(html)) return this.opts.emptyHtml;
7067        else return true;
7068    },
7069    _isDisabled: function() {
7070        return (this.opts.paragraphize === false || this.element.isType('inline'));
7071    },
7072    _isEmptyHtml: function(html) {
7073        return (html === '' || html === '<p></p>' || html === '<div></div>');
7074    }
7075});
7076$R.add('service', 'detector', {
7077    init: function(app)
7078    {
7079        this.app = app;
7080
7081        // local
7082        this.userAgent = navigator.userAgent.toLowerCase();
7083    },
7084	isWebkit: function()
7085	{
7086		return /webkit/.test(this.userAgent);
7087	},
7088	isFirefox: function()
7089	{
7090		return (this.userAgent.indexOf('firefox') > -1);
7091	},
7092	isIe: function(v)
7093	{
7094        if (document.documentMode || /Edge/.test(navigator.userAgent)) return 'edge';
7095
7096		var ie;
7097		ie = RegExp('msie' + (!isNaN(v)?('\\s'+v):''), 'i').test(navigator.userAgent);
7098		if (!ie) ie = !!navigator.userAgent.match(/Trident.*rv[ :]*11\./);
7099
7100		return ie;
7101	},
7102	isMobile: function()
7103	{
7104		return /(iPhone|iPod|Android)/.test(navigator.userAgent);
7105	},
7106	isDesktop: function()
7107	{
7108		return !/(iPhone|iPod|iPad|Android)/.test(navigator.userAgent);
7109	},
7110	isIpad: function()
7111	{
7112		return /iPad/.test(navigator.userAgent);
7113	}
7114});
7115$R.add('service', 'offset', {
7116    init: function(app)
7117    {
7118        this.app = app;
7119    },
7120    get: function(el, trimmed)
7121    {
7122        var offset = { start: 0, end: 0 };
7123        var node = this.utils.getNode(el);
7124        if (!node) return false;
7125
7126        var isEditor = this.editor.isEditor(node);
7127        var isIn = (isEditor) ? true : this.selection.isIn(node);
7128        var range = this.selection.getRange();
7129
7130        if (!isEditor && !isIn)
7131        {
7132            offset = false;
7133        }
7134        else if (this.selection.is() && isIn)
7135        {
7136            var $startNode = $R.dom(range.startContainer);
7137            var fix = ($startNode.hasClass('redactor-component')) ? range.startOffset : 0;
7138            var clonedRange = range.cloneRange();
7139
7140            clonedRange.selectNodeContents(node);
7141            clonedRange.setEnd(range.startContainer, range.startOffset);
7142
7143            var selection = this._getString(range, trimmed);
7144
7145            offset.start = this._getString(clonedRange, trimmed).length - fix;
7146            offset.end = offset.start + selection.length + fix;
7147        }
7148
7149        return offset;
7150    },
7151    set: function(offset, el)
7152    {
7153        if (this._setComponentOffset(el)) return;
7154
7155        this.component.clearActive();
7156        var node = this.utils.getNode(el);
7157        if (!node) return;
7158
7159        var size = this.size(node);
7160        var charIndex = 0, range = document.createRange();
7161
7162        offset.end = (offset.end > size) ? size : offset.end;
7163
7164        range.setStart(node, 0);
7165        range.collapse(true);
7166
7167        var nodeStack = [node], foundStart = false, stop = false;
7168        while (!stop && (node = nodeStack.pop()))
7169        {
7170            if (node.nodeType === 3)
7171            {
7172                var nextCharIndex = charIndex + node.length;
7173
7174                if (!foundStart && !this._isFigcaptionNext(node) && offset.start >= charIndex && offset.start <= nextCharIndex)
7175                {
7176                    range.setStart(node, offset.start - charIndex);
7177                    foundStart = true;
7178                }
7179
7180                if (foundStart && offset.end >= charIndex && offset.end <= nextCharIndex)
7181                {
7182                    range.setEnd(node, offset.end - charIndex);
7183                    stop = true;
7184                }
7185
7186                charIndex = nextCharIndex;
7187            }
7188            else
7189            {
7190                var i = node.childNodes.length;
7191                while (i--)
7192                {
7193                    nodeStack.push(node.childNodes[i]);
7194                }
7195            }
7196        }
7197
7198        this.selection.setRange(range);
7199    },
7200    size: function(el, trimmed)
7201    {
7202        var node = this.utils.getNode(el);
7203        if (node)
7204        {
7205            var range = document.createRange();
7206
7207            var clonedRange = range.cloneRange();
7208            clonedRange.selectNodeContents(node);
7209
7210            return this._getString(clonedRange, trimmed).length;
7211        }
7212
7213        return 0;
7214    },
7215
7216    // private
7217    _getString: function(obj, trimmed)
7218    {
7219        var str = obj.toString();
7220        str = (this.editor.isEmpty()) ? str.replace(/\uFEFF/g, '') : str;
7221        str = (trimmed) ? str.trim() : str;
7222
7223        return str;
7224    },
7225    _setComponentOffset: function(el)
7226    {
7227        return (this.component.isNonEditable(el)) ? this.component.setActive(el) : false;
7228    },
7229    _isFigcaptionNext: function(node)
7230    {
7231        var next = node.nextSibling;
7232        return (node.nodeValue.trim() === '' && next && next.tagName === 'FIGCAPTION');
7233    }
7234});
7235$R.add('service', 'inspector', {
7236    init: function(app)
7237    {
7238        this.app = app;
7239    },
7240    // parse
7241    parse: function(el)
7242    {
7243        return $R.create('inspector.parser', this.app, this, el);
7244    },
7245
7246    // text detection
7247    isText: function(el)
7248    {
7249        if (typeof el === 'string' && !/^\s*<(\w+|!)[^>]*>/.test(el))
7250        {
7251            return true;
7252        }
7253
7254        var node = $R.dom(el).get();
7255        return (node && node.nodeType === 3); //  && !this.selection.getBlock(el)
7256    },
7257
7258    // tag detection
7259    isInlineTag: function(tag, extend)
7260    {
7261        var tags = this._extendTags(this.opts.inlineTags, extend);
7262
7263        return (this._isTag(tag) && tags.indexOf(tag.toLowerCase()) !== -1);
7264    },
7265    isBlockTag: function(tag, extend)
7266    {
7267        var tags = this._extendTags(this.opts.blockTags, extend);
7268
7269        return (this._isTag(tag) && tags.indexOf(tag.toLowerCase()) !== -1);
7270    },
7271    isTableCellTag: function(tag)
7272    {
7273        return (['td', 'th'].indexOf(tag.toLowerCase()) !== -1);
7274    },
7275    isHeadingTag: function(tag)
7276    {
7277        return (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].indexOf(tag.toLowerCase()) !== -1);
7278    },
7279
7280
7281    _isTag: function(tag)
7282    {
7283        return (tag !== undefined && tag);
7284    },
7285    _extendTags: function(tags, extend)
7286    {
7287        tags = tags.concat(tags);
7288
7289        if (extend)
7290        {
7291            for (var i = 0 ; i < extend.length; i++)
7292            {
7293                tags.push(extend[i]);
7294            }
7295        }
7296
7297        return tags;
7298    }
7299});
7300$R.add('class', 'inspector.parser', {
7301    init: function(app, inspector, el)
7302    {
7303        this.app = app;
7304        this.uuid = app.uuid;
7305        this.opts = app.opts;
7306        this.utils = app.utils;
7307        this.editor = app.editor;
7308        this.selection = app.selection;
7309        this.inspector = inspector;
7310
7311        // local
7312        var $editor = this.editor.getElement();
7313        this.el = el;
7314        this.$el = $R.dom(this.el, $editor);
7315        this.node = this.$el.get();
7316
7317        // comment node
7318        if (this.node && this.node.nodeType === 8)
7319        {
7320            this.node = false;
7321        }
7322
7323        this.$component = this.$el.closest('.redactor-component', $editor);
7324    },
7325    // is
7326    isEditor: function()
7327    {
7328        return (this.node === this.editor.getElement().get());
7329    },
7330    isInEditor: function()
7331    {
7332        return (this.$el.parents('.redactor-in-' + this.uuid).length !== 0);
7333    },
7334    isComponent: function()
7335    {
7336        return (this.$component.length !== 0);
7337    },
7338    isComponentType: function(type)
7339    {
7340        return (this.getComponentType() === type);
7341    },
7342    isComponentActive: function()
7343    {
7344        return (this.isComponent() && this.$component.hasClass('redactor-component-active'));
7345    },
7346    isComponentEditable: function()
7347    {
7348        var types = ['code', 'table'];
7349        var type = this.getComponentType();
7350
7351        return (this.isComponent() && types.indexOf(type) !== -1);
7352    },
7353    isFigcaption: function()
7354    {
7355        return this.getFigcaption();
7356    },
7357    isPre: function()
7358    {
7359        return this.getPre();
7360    },
7361    isCode: function()
7362    {
7363        var $code = this.$el.closest('code');
7364        var $parent = $code.parent('pre');
7365
7366        return ($code.length !== 0 && $parent.length === 0);
7367    },
7368    isList: function()
7369    {
7370        return this.getList();
7371    },
7372    isFirstListItem: function()
7373    {
7374        return this._getLastOrFirstListItem('first');
7375    },
7376    isLastListItem: function()
7377    {
7378        return this._getLastOrFirstListItem('last');
7379    },
7380    isFirstTableCell: function()
7381    {
7382        return this._getLastOrFirstTableCell('first');
7383    },
7384    isLastTableCell: function()
7385    {
7386        return this._getLastOrFirstTableCell('last');
7387    },
7388    isTable: function()
7389    {
7390        return (this.isComponentType('table') || this.getTable());
7391    },
7392    isHeading: function()
7393    {
7394        return this.getHeading();
7395    },
7396    isBlockquote: function()
7397    {
7398        return this.getBlockquote();
7399    },
7400    isDl: function()
7401    {
7402        return this.getDl();
7403    },
7404    isParagraph: function()
7405    {
7406        return this.getParagraph();
7407    },
7408    isLink: function()
7409    {
7410        return this.getLink();
7411    },
7412    isFile: function()
7413    {
7414        return this.getFile();
7415    },
7416    isText: function()
7417    {
7418        return this.inspector.isText(this.el);
7419    },
7420    isInline: function()
7421    {
7422        var tags = this.opts.inlineTags;
7423
7424        return (this.isElement()) ? (tags.indexOf(this.node.tagName.toLowerCase()) !== -1) : false;
7425    },
7426    isBlock: function()
7427    {
7428        var tags = this.opts.blockTags;
7429
7430        return (this.isElement()) ? (tags.indexOf(this.node.tagName.toLowerCase()) !== -1) : false;
7431    },
7432    isElement: function()
7433    {
7434        return (this.node && this.node.nodeType && this.node.nodeType !== 3);
7435    },
7436
7437    // has
7438    hasParent: function(tags)
7439    {
7440        return (this.$el.closest(tags.join(',')).length !== 0);
7441    },
7442
7443    // get
7444    getNode: function()
7445    {
7446        return this.node;
7447    },
7448    getTag: function()
7449    {
7450        return (this.isElement()) ? this.node.tagName.toLowerCase() : false;
7451    },
7452    getComponent: function()
7453    {
7454        return (this.isComponent()) ? this.$component.get() : false;
7455    },
7456    getComponentType: function()
7457    {
7458        return (this.isComponent()) ? this.$component.attr('data-redactor-type') : false;
7459    },
7460    getFirstNode: function()
7461    {
7462        return this.utils.getFirstNode(this.node);
7463    },
7464    getLastNode: function()
7465    {
7466        return this.utils.getLastNode(this.node);
7467    },
7468    getFirstElement: function()
7469    {
7470        return this.utils.getFirstElement(this.node);
7471    },
7472    getLastElement: function()
7473    {
7474        return this.utils.getLastElement(this.node);
7475    },
7476    getFigcaption: function()
7477    {
7478        return this._getClosestNode('figcaption');
7479    },
7480    getPre: function()
7481    {
7482        return this._getClosestNode('pre');
7483    },
7484    getCode: function()
7485    {
7486        return this._getClosestNode('code');
7487    },
7488    getList: function()
7489    {
7490        return this._getClosestNode('ul, ol');
7491    },
7492    getParentList: function()
7493    {
7494        return this._getClosestUpNode('ul, ol');
7495    },
7496    getListItem: function()
7497    {
7498        return this._getClosestNode('li');
7499    },
7500    getTable: function()
7501    {
7502        if (this.getComponentType('table'))
7503        {
7504            return this.$component.find('table').get();
7505        }
7506        else
7507        {
7508            return this._getClosestNode('table');
7509        }
7510    },
7511    getTableCell: function()
7512    {
7513        var $td = this.$el.closest('td, th');
7514
7515        return ($td.length !== 0) ? $td.get() : false;
7516    },
7517    getComponentCodeElement: function()
7518    {
7519        return (this.isComponentType('code')) ? this.$component.find('pre code, pre').last().get() : false;
7520    },
7521    getImageElement: function()
7522    {
7523        return (this.isComponentType('image')) ? this.$component.find('img').get() : false;
7524    },
7525    getParagraph: function()
7526    {
7527        return this._getClosestNode('p');
7528    },
7529    getHeading: function()
7530    {
7531        return this._getClosestNode('h1, h2, h3, h4, h5, h6');
7532    },
7533    getDl: function()
7534    {
7535        return this._getClosestNode('dl');
7536    },
7537    getBlockquote: function()
7538    {
7539        return this._getClosestNode('blockquote');
7540    },
7541    getLink: function()
7542    {
7543        var isComponent = (this.isComponent() && !this.isFigcaption());
7544        var isTable = this.isComponentType('table');
7545
7546        if (isTable || !isComponent)
7547        {
7548            var $el = this._getClosestElement('a');
7549
7550            return ($el && !$el.attr('data-file')) ? $el.get() : false;
7551        }
7552
7553        return false;
7554    },
7555    getFile: function()
7556    {
7557        var isComponent = this.isComponent();
7558        var isTable = this.isComponentType('table');
7559
7560        if (isTable || !isComponent)
7561        {
7562            var $el = this._getClosestElement('a');
7563
7564            return ($el && $el.attr('data-file')) ? $el.get() : false;
7565        }
7566
7567        return false;
7568    },
7569
7570    // find
7571    findFirstNode: function(selector)
7572    {
7573        return this.$el.find(selector).first().get();
7574    },
7575    findLastNode: function(selector)
7576    {
7577        return this.$el.find(selector).last().get();
7578    },
7579
7580    // private
7581    _getLastOrFirstListItem: function(type)
7582    {
7583        var list = this.getList();
7584        var tag = this.getTag();
7585        if (list && tag === 'li')
7586        {
7587            var item = $R.dom(list).find('li')[type]().get();
7588            if (item && this.node === item)
7589            {
7590                return true;
7591            }
7592        }
7593
7594        return false;
7595    },
7596    _getLastOrFirstTableCell: function(type)
7597    {
7598        var table = this.getTable();
7599        var tag = this.getTag();
7600        if (table && (tag === 'td' || tag === 'th'))
7601        {
7602            var item = $R.dom(table).find('td, th')[type]().get();
7603            if (item && this.node === item)
7604            {
7605                return true;
7606            }
7607        }
7608
7609        return false;
7610    },
7611    _getClosestUpNode: function(selector)
7612    {
7613        var $editor = this.editor.getElement();
7614        var $el = this.$el.parents(selector, $editor).last();
7615
7616        return ($el.length !== 0) ? $el.get() : false;
7617    },
7618    _getClosestNode: function(selector)
7619    {
7620        var $editor = this.editor.getElement();
7621        var $el = this.$el.closest(selector, $editor);
7622
7623        return ($el.length !== 0) ? $el.get() : false;
7624    },
7625    _getClosestElement: function(selector)
7626    {
7627        var $editor = this.editor.getElement();
7628        var $el = this.$el.closest(selector, $editor);
7629
7630        return ($el.length !== 0) ? $el : false;
7631    }
7632});
7633$R.add('service', 'marker', {
7634    init: function(app)
7635    {
7636        this.app = app;
7637    },
7638    build: function(pos, html)
7639    {
7640        var marker = document.createElement('span');
7641
7642        marker.id = 'selection-marker-' + this._getPos(pos);
7643        marker.className = 'redactor-selection-marker';
7644        marker.innerHTML = this.opts.markerChar;
7645
7646        return (html) ? marker.outerHTML : marker;
7647    },
7648    buildHtml: function(pos)
7649    {
7650        return this.build(pos, true);
7651    },
7652    insert: function(side)
7653    {
7654        this.remove();
7655
7656        var atStart = (side !== 'both' && (side === 'start' || this.selection.isCollapsed()));
7657
7658        if (!this.selection.is()) this.editor.focus();
7659
7660        var range = this.selection.getRange();
7661        if (range)
7662        {
7663            var start = this.build('start');
7664            var end = this.build('end');
7665
7666            var cloned = range.cloneRange();
7667
7668            if (!atStart)
7669            {
7670                cloned.collapse(false);
7671                cloned.insertNode(end);
7672            }
7673
7674            cloned.setStart(range.startContainer, range.startOffset);
7675            cloned.collapse(true);
7676            cloned.insertNode(start);
7677
7678            range.setStartAfter(start);
7679
7680            if (!atStart)
7681            {
7682                range.setEndBefore(end);
7683            }
7684
7685            this.selection.setRange(range);
7686
7687            return start;
7688        }
7689    },
7690    find: function(pos, $context)
7691    {
7692        var $editor = this.editor.getElement();
7693        var $marker = ($context || $editor).find('span#selection-marker-' + this._getPos(pos));
7694
7695        return ($marker.length !== 0) ? $marker.get() : false;
7696    },
7697    restore: function()
7698    {
7699        var start = this.find('start');
7700        var end = this.find('end');
7701
7702        var range = this.selection.getRange();
7703        if (!range || !this.selection.is())
7704        {
7705            this.editor.focus();
7706            range = document.createRange();
7707        }
7708
7709        if (start)
7710        {
7711            var prev = (end) ? end.previousSibling : false;
7712            var next = start.nextSibling;
7713            next = (next && next.nodeType === 3 && next.textContent.replace(/[\n\t]/g, '') === '') ? false : next;
7714
7715            if (!end)
7716            {
7717                if (next)
7718                {
7719                    range.selectNodeContents(next);
7720                    range.collapse(true);
7721                }
7722                else
7723                {
7724                    this._restoreInject(range, start);
7725                }
7726            }
7727            else if (next && next.id === 'selection-marker-end')
7728            {
7729                this._restoreInject(range, start);
7730            }
7731            else
7732            {
7733                if (prev && next)
7734                {
7735                    range.selectNodeContents(prev);
7736                    range.collapse(false);
7737                    range.setStart(next, 0);
7738                }
7739                else if (prev && !next)
7740                {
7741                    range.selectNodeContents(prev);
7742                    range.collapse(false);
7743                    range.setStartAfter(start);
7744                }
7745                else
7746                {
7747                    range.setStartAfter(start);
7748                    range.setEndBefore(end);
7749                }
7750            }
7751
7752            this.selection.setRange(range);
7753
7754            if (start) start.parentNode.removeChild(start);
7755            if (end) end.parentNode.removeChild(end);
7756        }
7757    },
7758    remove: function()
7759    {
7760        var start = this.find('start');
7761        var end = this.find('end');
7762
7763        if (start) start.parentNode.removeChild(start);
7764        if (end) end.parentNode.removeChild(end);
7765    },
7766
7767    // private
7768    _getPos: function(pos)
7769    {
7770        return (pos === undefined) ? 'start' : pos;
7771    },
7772    _restoreInject: function(range, start)
7773    {
7774        var textNode = this.utils.createInvisibleChar();
7775        $R.dom(start).after(textNode);
7776
7777        range.selectNodeContents(textNode);
7778        range.collapse(false);
7779    }
7780});
7781$R.add('service', 'component', {
7782    init: function(app)
7783    {
7784        this.app = app;
7785
7786        // local
7787        this.activeClass = 'redactor-component-active';
7788    },
7789    create: function(type, el)
7790    {
7791        return $R.create(type + '.component', this.app, el);
7792    },
7793    build: function(el)
7794    {
7795        var $el = $R.dom(el);
7796        var component;
7797        var type = $el.attr('data-redactor-type');
7798        if (type)
7799        {
7800            component = this.create(type, el);
7801        }
7802
7803        return (component) ? component : el;
7804    },
7805    remove: function(el, caret)
7806    {
7807        var $component = $R.dom(el).closest('.redactor-component');
7808        var type = $component.attr('data-redactor-type');
7809        var current = $component.parent();
7810        var data = this.inspector.parse(current);
7811        var prev = this.utils.findSiblings($component, 'prev');
7812        var next = this.utils.findSiblings($component, 'next');
7813        var stop = this.app.broadcast(type + '.delete', $component);
7814        if (stop !== false)
7815        {
7816            $component.remove();
7817
7818            // callback
7819            this.app.broadcast(type + '.deleted', $component);
7820            this.app.broadcast('contextbar.close');
7821            this.app.broadcast('imageresizer.stop');
7822
7823            if (caret !== false)
7824            {
7825                var cell = data.getTableCell();
7826                if (cell && this.utils.isEmptyHtml(cell.innerHTML))
7827                {
7828                    this.caret.setStart(cell);
7829                }
7830                else if (next) this.caret.setStart(next);
7831                else if (prev) this.caret.setEnd(prev);
7832                else
7833                {
7834                    this.editor.startFocus();
7835                }
7836            }
7837
7838            // is empty
7839            if (this.editor.isEmpty())
7840            {
7841                this.editor.setEmpty();
7842                this.editor.startFocus();
7843                this.app.broadcast('empty');
7844            }
7845        }
7846    },
7847    isNonEditable: function(el)
7848    {
7849        var data = this.inspector.parse(el);
7850        return (data.isComponent() && !data.isComponentEditable());
7851    },
7852    isActive: function(el)
7853    {
7854        var $component;
7855        if (el)
7856        {
7857            var data = this.inspector.parse(el);
7858            $component = $R.dom(data.getComponent());
7859
7860            return $component.hasClass(this.activeClass);
7861        }
7862        else
7863        {
7864            $component = this._find();
7865
7866            return ($component.length !== 0);
7867        }
7868    },
7869    getActive: function(dom)
7870    {
7871        var $component = this._find();
7872
7873        return ($component.length !== 0) ? ((dom) ? $component : $component.get()) : false;
7874    },
7875    setActive: function(el)
7876    {
7877        this.clearActive();
7878        this.editor.focus();
7879
7880        var data = this.inspector.parse(el);
7881        var component = data.getComponent();
7882        var $component = $R.dom(component);
7883
7884        if (!data.isFigcaption())
7885        {
7886            var $caret = $component.find('.redactor-component-caret');
7887            if ($caret.length === 0)
7888            {
7889                $caret = this._buildCaret();
7890                $component.prepend($caret);
7891            }
7892
7893            this.caret.setAtStart($caret.get());
7894        }
7895
7896        $component.addClass(this.activeClass);
7897    },
7898    clearActive: function()
7899    {
7900        var $component = this._find();
7901
7902        $component.removeClass(this.activeClass);
7903        $component.find('.redactor-component-caret').remove();
7904
7905        this.app.broadcast('imageresizer.stop');
7906    },
7907    setOnEvent: function(e, contextmenu)
7908    {
7909        this.clearActive();
7910
7911        var data = this.inspector.parse(e.target);
7912        if (data.isFigcaption() || data.isComponentEditable())
7913        {
7914            return;
7915        }
7916
7917        // component
7918        if (data.isComponent())
7919        {
7920            this.setActive(e.target);
7921            if (contextmenu !== true) e.preventDefault();
7922        }
7923    },
7924    executeScripts: function(scripts)
7925    {
7926        if (scripts === undefined)
7927        {
7928            var $editor = this.editor.getElement();
7929            var scripts = $editor.find('[data-redactor-type]').find("script").getAll();
7930            this.executeScripts.call(this, scripts);
7931        }
7932        else
7933        {
7934            for (var i = 0; i < scripts.length; i++)
7935            {
7936                if (scripts[i].src !== '')
7937                {
7938                    var src = scripts[i].src;
7939                    this.$doc.find('head script[src="' + src + '"]').remove();
7940
7941                    var $script = $R.dom('<script>');
7942                    $script.attr('src', src);
7943                    $script.attr('async defer');
7944                    $script.get().onload = function()
7945                    {
7946                        if (src.search('instagram') !== -1) window.instgrm.Embeds.process();
7947                        this.executeScripts(scripts.slice(i + 1));
7948                    }.bind(this);
7949
7950                    var head = document.getElementsByTagName('head')[0];
7951                    if (head) head.appendChild($script.get());
7952
7953                    break;
7954                }
7955            }
7956        }
7957    },
7958
7959    // private
7960    _find: function()
7961    {
7962        return this.editor.getElement().find('.' + this.activeClass);
7963    },
7964    _buildCaret: function()
7965    {
7966        var $caret = $R.dom('<span>');
7967        $caret.addClass('redactor-component-caret');
7968        $caret.attr('contenteditable', true);
7969
7970        return $caret;
7971    }
7972});
7973$R.add('service', 'insertion', {
7974    init: function(app)
7975    {
7976        this.app = app;
7977    },
7978    set: function(html, clean, focus)
7979    {
7980        if (html === null) {
7981            html = '';
7982        }
7983
7984        html = (clean !== false) ? this.cleaner.input(html) : html;
7985        html = (clean !== false) ? this.cleaner.paragraphize(html) : html;
7986
7987        // set html
7988        var $editor = this.editor.getElement();
7989        $editor.html(html);
7990
7991        // set focus at the end
7992        if (focus !== false) this.editor.endFocus();
7993
7994        return html;
7995    },
7996    insertNode: function(node, caret)
7997    {
7998        this.editor.focus();
7999        var fragment = (this.utils.isFragment(node)) ? node : this.utils.createFragment(node);
8000
8001        this._collapseSelection();
8002        this._insertFragment(fragment);
8003        this._setCaret(caret, fragment);
8004
8005        return this._sendNodes(fragment.nodes);
8006    },
8007    insertBreakLine: function()
8008    {
8009        return this.insertNode(document.createElement('br'), 'after');
8010    },
8011    insertNewline: function()
8012    {
8013        return this.insertNode(document.createTextNode('\n'), 'after');
8014    },
8015    insertText: function(text)
8016    {
8017        return this.insertHtml(this.cleaner.getFlatText(text));
8018    },
8019    insertChar: function(charhtml)
8020    {
8021        return this.insertNode(charhtml, 'after');
8022    },
8023    insertRaw: function(html)
8024    {
8025        return this.insertHtml(html, false);
8026    },
8027    insertToEnd: function(lastNode, type)
8028    {
8029        if (!lastNode) return;
8030        if (lastNode.nodeType === 3 && lastNode.nodeValue.search(/^\n/) !== -1)
8031        {
8032            lastNode = lastNode.previousElementSibling;
8033        }
8034
8035        var $lastNode = $R.dom(lastNode);
8036        if ($lastNode.attr('data-redactor-type') === type)
8037        {
8038            var tag = (this.opts.breakline) ? '<br>' : '<p>';
8039            var $newNode = $R.dom(tag);
8040
8041            $lastNode.after($newNode);
8042            this.caret.setStart($newNode);
8043        }
8044    },
8045    insertPoint: function(e)
8046    {
8047        var range, data;
8048        var marker = this.marker.build('start');
8049        var markerInserted = false;
8050        var x = e.clientX, y = e.clientY;
8051
8052        if (document.caretPositionFromPoint)
8053        {
8054            var pos = document.caretPositionFromPoint(x, y);
8055            var sel = document.getSelection();
8056
8057            data = this.inspector.parse(pos.offsetNode);
8058            if (data.isInEditor())
8059            {
8060                range = sel.getRangeAt(0);
8061                range.setStart(pos.offsetNode, pos.offset);
8062                range.collapse(true);
8063                range.insertNode(marker);
8064                markerInserted = true;
8065            }
8066        }
8067        else if (document.caretRangeFromPoint)
8068        {
8069            range = document.caretRangeFromPoint(x, y);
8070
8071            data = this.inspector.parse(range.startContainer);
8072            if (data.isInEditor())
8073            {
8074                range.insertNode(marker);
8075                markerInserted = true;
8076            }
8077        }
8078
8079        return markerInserted;
8080    },
8081    insertToPoint: function(e, html, point, clean)
8082    {
8083        var pointInserted = (point === true) ? true : this.insertPoint(e);
8084        if (!pointInserted)
8085        {
8086            var lastNode = this.editor.getLastNode();
8087            $R.dom(lastNode).after(this.marker.build('start'));
8088        }
8089
8090        this.component.clearActive();
8091        this.selection.restoreMarkers();
8092
8093        return this.insertHtml(html, clean);
8094    },
8095    insertToOffset: function(start, html)
8096    {
8097        this.offset.set({ start: start, end: start });
8098
8099        return this.insertHtml(html);
8100    },
8101    insertHtml: function(html, clean)
8102    {
8103        if (!this.opts.input) return;
8104
8105        // parse
8106        var parsedInput = this.utils.parseHtml(html);
8107
8108        // all selection
8109        if (this.selection.isAll())
8110        {
8111            return this._insertToAllSelected(parsedInput);
8112        }
8113
8114        // there is no selection
8115        if (!this.selection.is())
8116        {
8117            var $el = $R.dom('<p>');
8118            var $editor = this.editor.getElement();
8119
8120            $editor.append($el);
8121            this.caret.setStart($el);
8122        }
8123
8124        // environment
8125        var isCollapsed = this.selection.isCollapsed();
8126        var isText = this.selection.isText();
8127        var current = this.selection.getCurrent();
8128        var block = this.selection.getBlock();
8129        var dataCurrent = this.inspector.parse(current);
8130
8131        // collapse air
8132        this._collapseSelection();
8133
8134        // clean
8135        parsedInput = this._getCleanedInput(parsedInput, dataCurrent, clean);
8136
8137        // input is figure or component span
8138        var isFigure = this._isFigure(parsedInput.html);
8139        var isComponentSpan = this._isComponentSpan(parsedInput.html);
8140        var isInsertedText = this.inspector.isText(parsedInput.html);
8141        var fragment, except;
8142
8143        // multiple selection
8144        //var blocks = this.selection.getBlocks();
8145        //if (blocks && blocks.length > 1) {
8146        //    parsedInput.html = (clean !== false) ? this.cleaner.paragraphize(parsedInput.html) : parsedInput.html;
8147        //}
8148
8149        // empty editor
8150        if (this.editor.isEmpty())
8151        {
8152            return this._insertToEmptyEditor(parsedInput.html);
8153        }
8154        // to component
8155        else if (dataCurrent.isComponent() && !dataCurrent.isComponentEditable())
8156        {
8157            return this._insertToWidget(current, dataCurrent, parsedInput.html);
8158        }
8159        // component span
8160        else if (isComponentSpan)
8161        {
8162            return this.insertNode(parsedInput.nodes, 'end');
8163        }
8164        // inserting figure & split node
8165        else if (isFigure && !isText && !dataCurrent.isList())
8166        {
8167            if (dataCurrent.isInline())
8168            {
8169                return this._insertToInline(current, parsedInput);
8170            }
8171
8172            fragment = this.utils.createFragment(parsedInput.html);
8173            this.utils.splitNode(current, fragment);
8174            this.caret.setEnd(fragment.last);
8175
8176            return this._sendNodes(fragment.nodes);
8177        }
8178        // to code
8179        else if (dataCurrent.isCode())
8180        {
8181            return this._insertToCode(parsedInput, current, clean);
8182        }
8183        // to pre
8184        else if (dataCurrent.isPre())
8185        {
8186            return this._insertToPre(parsedInput, clean);
8187        }
8188        // to h1-h6 & figcaption
8189        else if (dataCurrent.isHeading() || dataCurrent.isFigcaption())
8190        {
8191            parsedInput.html = (clean !== false) ? this.cleaner.removeTagsExcept(parsedInput.html, ['a']) : parsedInput.html;
8192            parsedInput.html = (clean !== false) ? this.cleaner.replaceNbspToSpaces(parsedInput.html) : parsedInput.html;
8193
8194            fragment = this.utils.createFragment(parsedInput.html);
8195
8196            return this.insertNode(fragment, 'end');
8197        }
8198        // breakline div
8199        else if (this.opts.breakline && block && block.tagName === 'DIV') {
8200
8201            if (this._isPlainHtml(parsedInput.html)) {
8202                return this.insertNode(parsedInput.nodes, 'end');
8203            }
8204
8205            parsedInput.html = (clean !== false) ? this.cleaner.paragraphize(parsedInput.html) : parsedInput.html;
8206
8207
8208            fragment = this.utils.createFragment(parsedInput.html);
8209
8210            var range = this.selection.getRange();
8211            if (range && !this.selection.isCollapsed()) {
8212                range.deleteContents();
8213            }
8214
8215            this.utils.splitNode(current, fragment);
8216            this.caret.setEnd(fragment.last);
8217
8218            return this._sendNodes(fragment.nodes);
8219        }
8220        // text inserting
8221        else if (isInsertedText)
8222        {
8223            if (!isText && this.opts.markup !== 'br' && this._hasBlocksAndImages(parsedInput.nodes))
8224            {
8225                parsedInput.html = (clean !== false) ? this.cleaner.paragraphize(parsedInput.html) : parsedInput.html;
8226
8227                fragment = this.utils.createFragment(parsedInput.html);
8228
8229                this.utils.splitNode(current, fragment);
8230                this.caret.setEnd(fragment.last);
8231                return this._sendNodes(fragment.nodes);
8232            }
8233
8234            parsedInput.html = (clean !== false) ? parsedInput.html.replace(/\n/g, '<br>') : parsedInput.html;
8235
8236            fragment = this.utils.createFragment(parsedInput.html);
8237
8238            return this.insertNode(fragment.nodes, 'end');
8239        }
8240        // uncollapsed
8241        else if (!isCollapsed && !isFigure)
8242        {
8243            if (this._isPlainHtml(parsedInput.html))
8244            {
8245                return this.insertNode(parsedInput.nodes, 'end');
8246            }
8247
8248            parsedInput.html = (clean !== false) ? this.cleaner.paragraphize(parsedInput.html) : parsedInput.html;
8249            fragment = this.utils.createFragment(parsedInput.html);
8250
8251            return this.insertNode(fragment, 'end');
8252        }
8253        // to inline tag
8254        else if (dataCurrent.isInline() && !this._isPlainHtml(parsedInput.html))
8255        {
8256            return this._insertToInline(current, parsedInput);
8257        }
8258        // to blockquote or dt, dd
8259        else if (dataCurrent.isBlockquote() || dataCurrent.isDl())
8260        {
8261            except = this.opts.inlineTags;
8262            except.concat(['br']);
8263
8264            parsedInput.html = (clean !== false) ? this.cleaner.replaceBlocksToBr(parsedInput.html) : parsedInput.html;
8265            parsedInput.html = (clean !== false) ? this.cleaner.removeTagsExcept(parsedInput.html, except) : parsedInput.html;
8266
8267            fragment = this.utils.createFragment(parsedInput.html);
8268
8269            return this.insertNode(fragment, 'end');
8270        }
8271        // to p
8272        else if (dataCurrent.isParagraph())
8273        {
8274            if (this._isPlainHtml(parsedInput.html))
8275            {
8276                return this.insertNode(parsedInput.nodes, 'end');
8277            }
8278
8279            parsedInput.html = (clean !== false) ? this.cleaner.paragraphize(parsedInput.html) : parsedInput.html;
8280
8281            fragment = this.utils.createFragment(parsedInput.html);
8282
8283            this.utils.splitNode(current, fragment);
8284            this.caret.setEnd(fragment.last);
8285
8286            return this._sendNodes(fragment.nodes);
8287        }
8288        // to li
8289        else if (dataCurrent.isList())
8290        {
8291            except = this.opts.inlineTags;
8292            except = except.concat(['br', 'li', 'ul', 'ol', 'img']);
8293
8294            parsedInput.html = (clean !== false) ? this.cleaner.replaceBlocksToBr(parsedInput.html) : parsedInput.html;
8295            parsedInput.html = (clean !== false) ? this.cleaner.removeTagsExcept(parsedInput.html, except) : parsedInput.html;
8296            parsedInput.html = (clean !== false) ? this.cleaner.removeBrAtEnd(parsedInput.html) : parsedInput.html;
8297
8298            fragment = this.utils.createFragment(parsedInput.html);
8299            parsedInput.nodes = fragment.nodes;
8300
8301            if (this._containsTags(parsedInput.html, ['ul', 'ol', 'li']))
8302            {
8303                var element = this.selection.getElement(current);
8304                if (element && element.tagName === 'LI' && this.caret.isStart(element))
8305                {
8306                    parsedInput.nodes = $R.dom(fragment.nodes).unwrap('ul, ol').getAll();
8307                    $R.dom(element).before(parsedInput.nodes);
8308
8309                    var lastNode = parsedInput.nodes[parsedInput.nodes.length-1];
8310                    this.caret.setEnd(lastNode);
8311
8312                    return this._sendNodes(parsedInput.nodes);
8313                }
8314                else if (this._isPlainHtml(parsedInput.html))
8315                {
8316                    return this.insertNode(fragment, 'end');
8317                }
8318                else
8319                {
8320                    fragment = this._buildList(parsedInput, element, fragment);
8321
8322                    this.utils.splitNode(current, fragment, true);
8323                    this.caret.setEnd(fragment.last);
8324
8325                    return this._sendNodes(fragment.nodes);
8326                }
8327            }
8328        }
8329
8330        // other cases
8331        return this.insertNode(parsedInput.nodes, 'end');
8332    },
8333
8334    // private
8335    _insertToAllSelected: function(parsedInput)
8336    {
8337        var insertedHtml = this.set(parsedInput.html);
8338        var dataInserted = this.utils.parseHtml(insertedHtml);
8339
8340        return this._sendNodes(dataInserted.nodes);
8341    },
8342    _insertToEmptyEditor: function(html)
8343    {
8344        html = this.cleaner.paragraphize(html);
8345
8346        var fragment = this.utils.createFragment(html);
8347        var $editor = this.editor.getElement();
8348
8349        $editor.html('');
8350        $editor.append(fragment.frag);
8351
8352        this.caret.setEnd(fragment.last);
8353
8354        return this._sendNodes(fragment.nodes);
8355    },
8356    _insertToInline: function(current, parsedInput)
8357    {
8358        var fragment = this.utils.createFragment(parsedInput.html);
8359        this.utils.splitNode(current, fragment, false, true);
8360        this.caret.setEnd(fragment.last);
8361
8362        return this._sendNodes(fragment.nodes);
8363    },
8364    _insertToCode: function(parsedInput, current, clean)
8365    {
8366        parsedInput.html = (clean !== false) ? this.cleaner.encodeHtml(parsedInput.html) : parsedInput.html;
8367        parsedInput.html = (clean !== false) ? this.cleaner.removeNl(parsedInput.html) : parsedInput.html;
8368
8369        var fragment = this.utils.createFragment(parsedInput.html);
8370        var nodes = this.insertNode(fragment, 'end');
8371
8372        this.utils.normalizeTextNodes(current);
8373
8374        return nodes;
8375    },
8376    _insertToPre: function(parsedInput, clean)
8377    {
8378        parsedInput.html = (clean !== false) ? this.cleaner.encodeHtml(parsedInput.html) : parsedInput.html;
8379
8380        var fragment = this.utils.createFragment(parsedInput.html);
8381
8382        return this.insertNode(fragment, 'end');
8383    },
8384    _insertToWidget: function(current, dataCurrent, html)
8385    {
8386        html = (this._isComponentSpan(html)) ? html : this.cleaner.paragraphize(html);
8387
8388        var fragment = this.utils.createFragment(html);
8389        var component = dataCurrent.getComponent();
8390        var $component = $R.dom(component);
8391
8392        $component.after(fragment.frag);
8393        $component.remove();
8394
8395        this.caret.setEnd(fragment.last);
8396
8397        return this._sendNodes(fragment.nodes);
8398    },
8399    _insertFragment: function(fragment)
8400    {
8401        var range = this.selection.getRange();
8402        if (range)
8403        {
8404            if (this.selection.isCollapsed())
8405            {
8406                var startNode = range.startContainer;
8407                if (startNode.nodeType !== 3 && startNode.tagName === 'BR')
8408                {
8409                    this.caret.setAfter(startNode);
8410                    startNode.parentNode.removeChild(startNode);
8411                }
8412            }
8413            else
8414            {
8415                range.deleteContents();
8416            }
8417
8418            range.insertNode(fragment.frag);
8419        }
8420    },
8421    _sendNodes: function(nodes)
8422    {
8423        for (var i = 0; i < nodes.length; i++)
8424        {
8425            var el = nodes[i];
8426            var type = (el.nodeType !== 3 && typeof el.getAttribute === 'function') ? el.getAttribute('data-redactor-type') : false;
8427            if (type)
8428            {
8429                this.app.broadcast(type + '.inserted', this.component.build(el));
8430            }
8431        }
8432
8433        if (this.detector.isIe())
8434        {
8435            this.editor.getElement().find('[data-redactor-type=table]').attr('contenteditable', true);
8436        }
8437
8438        // callback
8439        this.app.broadcast('inserted', nodes);
8440
8441        // widget's scripts
8442        this.component.executeScripts();
8443
8444        return nodes;
8445    },
8446    _setCaret: function(caret, fragment)
8447    {
8448        var isLastInline = this._isLastInline(fragment);
8449
8450        if (caret)
8451        {
8452            caret = (isLastInline && caret === 'end') ? 'after' : caret;
8453            this.caret['set' + this.utils.ucfirst(caret)](fragment.last);
8454        }
8455        else if (caret !== false)
8456        {
8457            if (isLastInline) this.caret.setAfter(fragment.last);
8458        }
8459    },
8460    _isLastInline: function(fragment)
8461    {
8462        if (fragment.last)
8463        {
8464            var data = this.inspector.parse(fragment.last);
8465
8466            return data.isInline();
8467        }
8468
8469        return false;
8470    },
8471    _getCleanedInput: function(parsedInput, dataCurrent, clean)
8472    {
8473        var isPreformatted = (dataCurrent.isCode() || dataCurrent.isPre());
8474
8475        parsedInput.html = parsedInput.html.replace(/&nbsp;/g, ' ');
8476        parsedInput.html = (!isPreformatted && clean !== false) ? this.cleaner.input(parsedInput.html) : parsedInput.html;
8477        parsedInput = (!isPreformatted && clean !== false) ? this.utils.parseHtml(parsedInput.html) : parsedInput;
8478
8479        return parsedInput;
8480    },
8481    _getContainer: function(nodes)
8482    {
8483        return $R.dom(this.utils.createTmpContainer(nodes));
8484    },
8485    _buildList: function(parsedInput, list, fragment)
8486    {
8487        var nodes = parsedInput.nodes;
8488        var first = nodes[0];
8489
8490        if (first && first.nodeType !== 3 && first.tagName === 'li')
8491        {
8492            var $parent = $R.dom(list);
8493            var parentListTag = $parent.get().tagName.toLowerCase();
8494            var $list = $R.dom('<' + parentListTag + ' />');
8495            $list.append(fragment.nodes);
8496
8497            return this.utils.createFragment($list.get().outerHTML);
8498        }
8499
8500        return fragment;
8501    },
8502    _containsTags: function(html, tags)
8503    {
8504        return (this._getContainer(html).find(tags.join(',')).length !== 0);
8505    },
8506    _collapseSelection: function()
8507    {
8508        //if (this.app.isAirToolbar()) this.selection.collapseToEnd();
8509    },
8510    _hasFigureOrTable: function(nodes)
8511    {
8512        return (this._getContainer(nodes).find('figure, table').length !== 0);
8513    },
8514    _hasBlocks: function(nodes)
8515    {
8516        return (this._getContainer(nodes).find(this.opts.blockTags.join(',')).length !== 0);
8517    },
8518    _hasBlocksAndImages: function(nodes)
8519    {
8520        return (this._getContainer(nodes).find(this.opts.blockTags.join(',') + ',img').length !== 0);
8521    },
8522    _isPlainHtml: function(html)
8523    {
8524        return (this._getContainer(html).find(this.opts.blockTags.join(',') + ', img').length === 0);
8525    },
8526    _isFigure: function(html)
8527    {
8528        if (this._isHtmlString(html))
8529        {
8530            return ($R.dom(html).closest('figure').length !== 0);
8531        }
8532    },
8533    _isComponentSpan: function(html)
8534    {
8535        if (this._isHtmlString(html))
8536        {
8537            return ($R.dom(html).closest('span.redactor-component').length !== 0);
8538        }
8539    },
8540    _isHtmlString: function(html)
8541    {
8542        return !(typeof html === 'string' && !/^\s*<(\w+|!)[^>]*>/.test(html));
8543    }
8544});
8545$R.add('service', 'block', {
8546    init: function(app) {
8547        this.app = app;
8548        this.tags = ['p', 'div', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
8549    },
8550    // format
8551    format: function(args) {
8552
8553        // params
8554        this.params = {
8555            args: false
8556        };
8557
8558        // type of applying styles and attributes
8559        this.params.type = (args.type) ? args.type : 'set'; // add, remove, toggle
8560
8561        // tag
8562        this.params.tag = (typeof args === 'string') ? args : (args.tag || this.opts.markup);
8563        this.params.tag = this.params.tag.toLowerCase();
8564
8565        // args
8566        this.params.args = {
8567            'class': args['class'] || false,
8568            'style': args['style'] || false,
8569            'attr': args['attr'] || false
8570        };
8571
8572        if (!args['class'] && !args['style'] && !args['attr']) {
8573            this.params.args = false;
8574        }
8575
8576        // format
8577        return this._format();
8578    },
8579
8580    // funcs
8581    add: function(args, tags, el) {
8582        return this._apply('add', args, tags, true, el);
8583    },
8584    set: function(args, tags) {
8585        return this._apply('set', args, tags);
8586    },
8587    toggle: function(args, tags) {
8588        return this._apply('toggle', args, tags);
8589    },
8590    remove: function(args, tags) {
8591        return this._apply('remove', args, tags);
8592    },
8593
8594    // clear
8595    clearFormat: function(tags) {
8596        return this._clear(tags, 'all');
8597    },
8598    clearStyle: function(tags) {
8599        return this._clear(tags, 'style');
8600    },
8601    clearClass: function(tags) {
8602        return this._clear(tags, 'class');
8603    },
8604    clearAttr: function(tags) {
8605        return this._clear(tags, 'attr');
8606    },
8607
8608    // format
8609    _format: function() {
8610
8611        var nodes = [];
8612
8613        // collapsed
8614        this.collapsed = this.selection.isCollapsed();
8615
8616        // save selection
8617        this.selection.save();
8618
8619        // blocks
8620        var block = this.selection.getBlock();
8621        var blocks = this._getBlocks();
8622        var type = (this._isToggleFormatType(blocks)) ? 'toggle' : 'set';
8623        var replacedTag = this._getReplacedTag(type);
8624
8625        nodes = this._replaceBlocks(blocks, replacedTag);
8626
8627        // apply args & clean
8628        nodes = this._buildNodes(nodes);
8629
8630        // restore selection
8631        this._restoreSelection(nodes);
8632
8633        return nodes;
8634    },
8635
8636
8637    // clear
8638    _clear: function(tags, type, selection, nodes) {
8639        // save selection
8640        if (selection !== false) {
8641		    this.selection.save();
8642		}
8643
8644        // get elements
8645        var $elements = this._getElements(tags, nodes);
8646
8647        // remove all format
8648        if (type === 'all') {
8649            this._removeAllAttr($elements, false);
8650        }
8651        // remove style
8652        else if (type === 'style') {
8653            $elements.removeAttr('style');
8654            $elements.removeAttr('data-redactor-style-cache');
8655        }
8656        // remove class
8657        else if (type === 'class') {
8658            $elements.removeAttr('class');
8659        }
8660        // remove attrs
8661        else if (type === 'attr') {
8662            this._removeAllAttr($elements);
8663        }
8664
8665        // get nodes
8666        nodes = $elements.getAll();
8667
8668        // restore selection
8669        if (selection !== false) {
8670            this._restoreSelection(nodes);
8671        }
8672
8673        return nodes;
8674    },
8675
8676    // get
8677    _getElements: function(tags, nodes) {
8678        return (nodes) ? $R.dom(nodes) : $R.dom(this._getBlocks(tags));
8679    },
8680    _getBlocks: function(tags) {
8681        var blocks = this.selection.getBlocks({ tags: tags || this.tags });
8682        var finalBlocks = [];
8683        for (var i = 0; i < blocks.length; i++) {
8684            if (blocks[i].tagName === 'DIV' && !blocks[i].getAttribute('data-redactor-tag')) {
8685                continue;
8686            }
8687            else {
8688                finalBlocks.push(blocks[i]);
8689            }
8690        }
8691
8692        return finalBlocks;
8693    },
8694
8695    _getReplacedTag: function(type) {
8696        if (this.opts.breakline) {
8697            return (type === 'toggle') ? 'div' : (this.params.tag === 'p') ? 'div' : this.params.tag;
8698        }
8699        else {
8700            return (type === 'toggle') ? this.opts.markup : this.params.tag;
8701        }
8702    },
8703
8704    // is
8705    _isStandardParagraph: function() {
8706        return (!this.opts.breakline && this.opts.markup === 'p');
8707    },
8708    _isStandardDiv: function() {
8709        return (!this.opts.breakline && this.opts.markup === 'div');
8710    },
8711    _isBreaklineBlock: function(block) {
8712        return (block && block.tagName === 'DIV' && block.getAttribute('data-redactor-tag') === 'br');
8713    },
8714    _isToggleFormatType: function(blocks) {
8715        var count = 0;
8716        var len = blocks.length;
8717        for (var i = 0; i < len; i++) {
8718            if (blocks[i] && this.params.tag === blocks[i].tagName.toLowerCase()) count++;
8719        }
8720
8721        return (count === len);
8722    },
8723    _isCurrentBlockOneAndEmpty: function(nodes) {
8724        return (this.collapsed && nodes.length === 1 && this.utils.isEmpty(nodes[0]));
8725    },
8726
8727    // build
8728    _buildNodes: function(nodes) {
8729        if (nodes.length > 0) {
8730            // clean & appliyng styles and attributes
8731            nodes = this._applyArgs(nodes, false);
8732            nodes = this._combinePre(nodes);
8733            nodes = this._cleanBlocks(nodes);
8734        }
8735
8736        return nodes;
8737    },
8738
8739    // replace
8740    _replaceBlocks: function(blocks, replacedTag) {
8741        var nodes = [];
8742
8743        for (var i = 0; i < blocks.length; i++) {
8744            var $node = this.utils.replaceToTag(blocks[i], replacedTag);
8745            nodes.push($node.get());
8746        }
8747
8748        return nodes;
8749    },
8750
8751    // combine
8752    _combinePre: function(nodes) {
8753        var combinedNodes = [];
8754        for (var i = 0; i < nodes.length; i++) {
8755            var next = nodes[i].nextElementSibling;
8756            if (next && nodes[i].tagName === 'PRE' && next.tagName === 'PRE') {
8757                var $current = $R.dom(nodes[i]);
8758                var $next = $R.dom(next);
8759                var newline = document.createTextNode('\n');
8760
8761                $current.append(newline);
8762                $current.append($next);
8763                $next.unwrap('pre');
8764            }
8765
8766            combinedNodes.push(nodes[i]);
8767        }
8768
8769        return combinedNodes;
8770    },
8771
8772    // clean
8773    _cleanBlocks: function(nodes) {
8774        var headings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
8775        var tags = this.opts.inlineTags;
8776        for (var i = 0; i < nodes.length; i++) {
8777            var tag = nodes[i].tagName.toLowerCase();
8778            var $node = $R.dom(nodes[i]);
8779
8780            // remove all spans in headings
8781            if (headings.indexOf(tag) !== - 1) {
8782                $node.find('span').not('.redactor-component, .non-editable, .redactor-selection-marker').unwrap();
8783            }
8784            // remove all inlines in pre
8785            else if (tag === 'pre') {
8786                $node.find(tags.join(',')).not('.redactor-selection-marker').unwrap();
8787            }
8788
8789
8790            if (this.params.args === false && this.params.tag === 'p') {
8791                $node.removeAttr('class');
8792            }
8793
8794            // breakline attr
8795            if (this.opts.breakline && tag === 'div') {
8796                $node.attr('data-redactor-tag', 'br');
8797            }
8798            else {
8799                $node.removeAttr('data-redactor-tag');
8800            }
8801
8802            // normalize
8803            this.utils.normalizeTextNodes(nodes[i]);
8804        }
8805
8806        return nodes;
8807    },
8808    _cleanEmptyClass: function($elements) {
8809        $elements.each(function(node) {
8810            if (node.className === '') node.removeAttribute('class');
8811        });
8812    },
8813    _cleanEmptyStyle: function($node) {
8814         if (this.utils.removeEmptyAttr($node.get(), 'style')) {
8815            $node.removeAttr('data-redactor-style-cache');
8816        }
8817        else {
8818            $node.attr('data-redactor-style-cache', $node.attr('style'));
8819        }
8820    },
8821
8822    // apply
8823    _apply: function(type, args, tags, selection, nodes) {
8824        // save selection
8825        if (selection !== false) {
8826		    this.selection.save();
8827		}
8828
8829        // get elements
8830        var $elements = this._getElements(tags, nodes);
8831
8832        // class
8833        if (args['class']) {
8834            if (type === 'set') {
8835                $elements.removeAttr('class');
8836                $elements.addClass(args['class']);
8837            }
8838            else if (type === 'add') {
8839                $elements.addClass(args['class']);
8840            }
8841            else if (type === 'toggle') {
8842                $elements.toggleClass(args['class']);
8843            }
8844            else if (type === 'remove') {
8845                $elements.removeClass(args['class']);
8846            }
8847
8848            this._cleanEmptyClass($elements);
8849        }
8850
8851        // attr
8852        if (args['attr']) {
8853            if (type === 'set') {
8854                this._removeAllAttr($elements);
8855                $elements.attr(args['attr']);
8856            }
8857            else if (type === 'add') {
8858                $elements.attr(args['attr']);
8859            }
8860            else if (type === 'toggle') {
8861                params = args['attr'];
8862                $elements.each(function(node) {
8863                    var $node = $R.dom(node);
8864                    for (var key in params) {
8865                        if ($node.attr(key)) $node.removeAttr(key);
8866                        else $node.attr(key, params[key]);
8867                    }
8868                });
8869            }
8870            else if (type === 'remove') {
8871                $elements.removeAttr(args['attr']);
8872            }
8873        }
8874
8875        // style
8876        if (args['style']) {
8877            if (type === 'set') {
8878                $elements.removeAttr('style');
8879                $elements.css(args['style']);
8880                $elements.each(function(node) {
8881                    var $node = $R.dom(node);
8882                    $node.attr('data-redactor-style-cache', $node.attr('style'));
8883                });
8884            }
8885            else if (type === 'add') {
8886                var params = args['style'];
8887                $elements.each(function(node) {
8888                    var $node = $R.dom(node);
8889                    $node.css(params);
8890                    $node.attr('data-redactor-style-cache', $node.attr('style'));
8891                    this._convertStyleQuotes($node);
8892                }.bind(this));
8893            }
8894            else if (type === 'toggle') {
8895                var params = args['style'];
8896                $elements.each(function(node) {
8897                    var $node = $R.dom(node);
8898                    for (var key in params) {
8899                        var newVal = params[key];
8900                        var oldVal = $node.css(key);
8901
8902                        oldVal = (this.utils.isRgb(oldVal)) ? this.utils.rgb2hex(oldVal) : oldVal.replace(/"/g, '');
8903                        newVal = (this.utils.isRgb(newVal)) ? this.utils.rgb2hex(newVal) : newVal.replace(/"/g, '');
8904
8905                        oldVal = this.utils.hex2long(oldVal);
8906                        newVal = this.utils.hex2long(newVal);
8907
8908                        var compareNew = (typeof newVal === 'string') ? newVal.toLowerCase() : newVal;
8909                        var compareOld = (typeof oldVal === 'string') ? oldVal.toLowerCase() : oldVal;
8910
8911                        if (compareNew === compareOld) $node.css(key, '');
8912                        else $node.css(key, newVal);
8913                    }
8914
8915                    this._convertStyleQuotes($node);
8916                    this._cleanEmptyStyle($node);
8917                }.bind(this));
8918            }
8919            else if (type === 'remove') {
8920                var name = args['style'];
8921                $elements.each(function(node) {
8922                    var $node = $R.dom(node);
8923                    $node.css(name, '');
8924                    this._cleanEmptyStyle($node);
8925                }.bind(this));
8926            }
8927        }
8928
8929        // get nodes
8930        nodes = $elements.getAll();
8931
8932        // restore selection
8933        if (selection !== false) {
8934            this._restoreSelection(nodes);
8935        }
8936
8937        return nodes;
8938    },
8939    _applyArgs: function(nodes) {
8940        if (this.params.args) {
8941            nodes = this._apply(this.params.type, this.params.args, false, false, nodes);
8942        }
8943        else {
8944            nodes = this._clear(false, 'all', false, nodes);
8945        }
8946
8947        return nodes;
8948    },
8949
8950    // remove
8951    _removeAllAttr: function($elements, keepStyleAndClass) {
8952        $elements.each(function(node) {
8953            var keepAttrs = ['data-redactor-tag', 'data-redactor-style-cache'];
8954            if (keepStyleAndClass === false) {
8955                keepAttrs.push('style');
8956                keepAttrs.push('class');
8957            }
8958
8959            for (var i = node.attributes.length; i--> 0;) {
8960                var nodeAttr = node.attributes[i];
8961                var name = nodeAttr.name;
8962                if (keepAttrs.indexOf(name) === -1) {
8963                    node.removeAttributeNode(nodeAttr);
8964                }
8965            }
8966        });
8967    },
8968
8969    // selection
8970    _restoreSelection: function(nodes) {
8971        if (this._isCurrentBlockOneAndEmpty(nodes)) {
8972            this.caret.setStart(nodes[0]);
8973        }
8974        else {
8975            setTimeout(function() { this.selection.restore(); }.bind(this), 1);
8976        }
8977    },
8978
8979    // convert
8980    _convertStyleQuotes: function($node) {
8981        var style = $node.attr('style');
8982        if (style) $node.attr('style', style.replace(/"/g, '\''));
8983    }
8984});
8985$R.add('service', 'inline', {
8986    mixins: ['formatter'],
8987    init: function(app)
8988    {
8989        this.app = app;
8990
8991        this.count = 0;
8992    },
8993    // public
8994    format: function(args)
8995    {
8996        if (!this._isFormat()) return [];
8997
8998        // type of applying styles and attributes
8999        this.type = (args.type) ? args.type : 'set'; // add, remove, toggle
9000        // tag
9001        this.tag = (typeof args === 'string') ? args : args.tag;
9002        this.tag = this.tag.toLowerCase();
9003        this.tag = this.arrangeTag(this.tag);
9004
9005        if (typeof args === 'string') this.args = false;
9006        else this.buildArgs(args);
9007
9008        if (!this.detector.isIe())
9009        {
9010            this.editor.disableNonEditables();
9011        }
9012
9013        // format
9014        var nodes = (this.selection.isCollapsed()) ? this.formatCollapsed() : this.formatUncollapsed();
9015
9016        if (!this.detector.isIe())
9017        {
9018            this.editor.enableNonEditables();
9019        }
9020
9021        return nodes;
9022    },
9023
9024    // private
9025    _isFormat: function()
9026    {
9027        var current = this.selection.getCurrent();
9028        var data = this.inspector.parse(current);
9029        var isComponent = (data.isComponent() && !data.isComponentType('table') && !data.isFigcaption());
9030
9031        if (current === false && this.selection.isAll())
9032        {
9033            return true;
9034        }
9035        else if (!current || data.isPre() || data.isCode() || isComponent)
9036        {
9037            return false;
9038        }
9039
9040        return true;
9041    },
9042    arrangeTag: function(tag)
9043    {
9044        var replaced = this.opts.replaceTags;
9045        for (var key in replaced)
9046        {
9047            if (tag === key) tag = replaced[key];
9048        }
9049
9050        return tag;
9051    },
9052    formatCollapsed: function()
9053    {
9054        var nodes = [];
9055        var inline = this.selection.getInlineFirst();
9056        var inlines = this.selection.getInlines({ all: true });
9057        var $inline = $R.dom(inline);
9058        var $parent, parent, $secondPart, extractedContent;
9059
9060        // 1) not inline
9061        if (!inline)
9062        {
9063            nodes = this.insertInline(nodes);
9064        }
9065        else
9066        {
9067            var dataInline = this.inspector.parse(inline);
9068            var isEmpty = this.utils.isEmptyHtml(inline.innerHTML);
9069
9070            // 2) inline is empty
9071            if (isEmpty)
9072            {
9073                // 2.1) has same tag
9074                if (inline.tagName.toLowerCase() === this.tag)
9075                {
9076                    // 2.1.1) has same args or hasn't args
9077                    if (this.hasSameArgs(inline))
9078                    {
9079                        this.caret.setAfter(inline);
9080                        $inline.remove();
9081
9082                        var el = this.selection.getElement();
9083                        this.utils.normalizeTextNodes(el);
9084                    }
9085                    // 2.1.2) has different args and it is span tag
9086                    else if (this.tag === 'span')
9087                    {
9088                        nodes = this.applyArgs([inline], false);
9089                        this.caret.setStart(inline);
9090                    }
9091                    // 2.1.3) has different args and it is not span tag
9092                    else
9093                    {
9094                       nodes = this.insertInline(nodes);
9095                    }
9096
9097                }
9098                // 2.2) has another tag
9099                else
9100                {
9101                    // 2.2.1) has parent
9102                    if (dataInline.hasParent([this.tag]))
9103                    {
9104                        $parent = $inline.closest(this.tag);
9105                        parent = $parent.get();
9106                        if (this.hasSameArgs(parent))
9107                        {
9108                            $parent.unwrap();
9109                            this.caret.setStart(inline);
9110                        }
9111                        else
9112                        {
9113                            nodes = this.insertInline(nodes);
9114                        }
9115                    }
9116                    // 2.2.2) hasn't parent
9117                    else
9118                    {
9119                        nodes = this.insertInline(nodes);
9120                    }
9121                }
9122            }
9123            // 3) inline isn't empty
9124            else
9125            {
9126                // 3.1) has same tag
9127                if (inline.tagName.toLowerCase() === this.tag)
9128                {
9129                    // 3.1.1) has same args or hasn't args
9130                    if (this.hasSameArgs(inline))
9131                    {
9132                        // insert break
9133                        extractedContent = this.utils.extractHtmlFromCaret(inline);
9134                        $secondPart = $R.dom('<' + this.tag + ' />');
9135                        $secondPart = this.utils.cloneAttributes(inline, $secondPart);
9136
9137                        $inline.after($secondPart.append(extractedContent));
9138
9139                        if ($secondPart.html().trim() === '') {
9140                            $secondPart.remove();
9141                        }
9142
9143                        this.caret.setAfter(inline);
9144                    }
9145                    else
9146                    {
9147                        nodes = this.insertInline(nodes);
9148                    }
9149                }
9150                // 3.2) has another tag
9151                else
9152                {
9153                    // 3.2.1) has parent
9154                    if (dataInline.hasParent([this.tag]))
9155                    {
9156                        $parent = $inline.closest(this.tag);
9157                        parent = $parent.get();
9158                        if (this.hasSameArgs(parent))
9159                        {
9160                            // insert break
9161                            extractedContent = this.utils.extractHtmlFromCaret(parent, parent);
9162                            $secondPart = $R.dom('<' + this.tag + ' />');
9163                            $secondPart = this.utils.cloneAttributes(parent, $secondPart);
9164
9165                            var $breaked, $last;
9166                            var z = 0;
9167                            inlines = inlines.reverse();
9168                            for (var i = 0; i < inlines.length; i++)
9169                            {
9170                                if (inlines[i] !== parent)
9171                                {
9172                                    $last = $R.dom('<' + inlines[i].tagName.toLowerCase() + '>');
9173                                    if (z === 0)
9174                                    {
9175                                        $breaked = $last;
9176                                    }
9177                                    else
9178                                    {
9179                                        $breaked.append($last);
9180                                    }
9181
9182                                    z++;
9183                                }
9184                            }
9185
9186                            $parent.after($secondPart.append(extractedContent));
9187                            $parent.after($breaked);
9188
9189                            this.caret.setStart($last);
9190                        }
9191                        else
9192                        {
9193                            nodes = this.insertInline(nodes);
9194                        }
9195                    }
9196                    // 3.2.2) hasn't parent
9197                    else
9198                    {
9199                        nodes = this.insertInline(nodes);
9200                    }
9201                }
9202            }
9203        }
9204
9205        return nodes;
9206    },
9207    insertInline: function(nodes)
9208    {
9209        var node = document.createElement(this.tag);
9210        nodes = this.insertion.insertNode(node, 'start');
9211
9212        return this.applyArgs(nodes, false);
9213    },
9214    hasSameArgs: function(inline)
9215    {
9216        if (inline.attributes.length === 0 && this.args === false)
9217        {
9218            return true;
9219        }
9220        else
9221        {
9222            var same = true;
9223            if (this.args)
9224            {
9225                var count = 0;
9226                for (var key in this.args)
9227                {
9228                    var $node = $R.dom(inline);
9229                    var args = (this.args[key]);
9230                    var value = this.utils.toParams(args);
9231                    var nodeAttrValue = $node.attr(key);
9232
9233                    if (args)
9234                    {
9235                        if (key === 'style')
9236                        {
9237                            value = value.trim().replace(/;$/, '');
9238
9239                            var origRules = this.utils.styleToObj($node.attr('style'));
9240                            var rules = value.split(';');
9241                            var innerCount = 0;
9242
9243                            for (var i = 0; i < rules.length; i++)
9244                            {
9245                                var arr = rules[i].split(':');
9246                                var ruleName = arr[0].trim();
9247                                var ruleValue = arr[1].trim();
9248
9249                                if (ruleName.search(/color/) !== -1)
9250                                {
9251                                    var val = $node.css(ruleName);
9252                                    if (val && (val === ruleValue || this.utils.rgb2hex(val) === ruleValue))
9253                                    {
9254                                        innerCount++;
9255                                    }
9256                                }
9257                                else if ($node.css(ruleName) === ruleValue)
9258                                {
9259                                    innerCount++;
9260                                }
9261                            }
9262
9263                            if (innerCount === rules.length && Object.keys(origRules).length === rules.length)
9264                            {
9265                                count++;
9266                            }
9267                        }
9268                        else
9269                        {
9270                            if (nodeAttrValue === value)
9271                            {
9272                                count++;
9273                            }
9274                        }
9275                    }
9276                    else
9277                    {
9278                        if (!nodeAttrValue || nodeAttrValue === '')
9279                        {
9280                            count++;
9281                        }
9282                    }
9283                }
9284
9285                same = (count === Object.keys(this.args).length);
9286            }
9287
9288            return same;
9289        }
9290    },
9291    formatUncollapsed: function()
9292    {
9293        var inlines = this.selection.getInlines({ all: true, inside: true });
9294
9295        if (this.detector.isIe()) this.selection.saveMarkers();
9296        else this.selection.save();
9297
9298        // convert del / u
9299        this._convertTags('u');
9300        this._convertTags('del');
9301
9302        // convert target tags
9303        this._convertToStrike(inlines);
9304
9305        if (this.detector.isIe()) this.selection.restoreMarkers();
9306        else this.selection.restore();
9307
9308        // apply strike
9309        document.execCommand('strikethrough');
9310
9311        // clear decoration
9312        this._clearDecoration();
9313
9314        this.selection.save();
9315
9316        // revert and set style
9317        var nodes = this._revertToInlines();
9318        nodes = this.applyArgs(nodes, false);
9319
9320        // unwrap if attributes was removed
9321        for (var i = 0; i < nodes.length; i++)
9322        {
9323            var node = nodes[i];
9324            var tag = node.tagName.toLowerCase();
9325            var len = node.attributes.length;
9326
9327            if (tag === this.tag && len === 0 && this.args)
9328            {
9329                $R.dom(node).unwrap();
9330                nodes.splice(i, 1);
9331            }
9332        }
9333
9334        this.selection.restore();
9335
9336        // clear and normalize
9337        this._clearEmptyStyle();
9338        nodes = this._normalizeBlocks(nodes);
9339
9340        return nodes;
9341    },
9342    _convertTags: function(tag)
9343    {
9344        if (this.tag !== tag)
9345        {
9346            var $editor = this.editor.getElement();
9347            $editor.find(tag).each(function(node)
9348            {
9349                var $el = this.utils.replaceToTag(node, 'span');
9350                $el.addClass('redactor-convertable-' + tag);
9351            }.bind(this));
9352        }
9353    },
9354    _revertTags: function(tag)
9355    {
9356        var $editor = this.editor.getElement();
9357
9358        $editor.find('span.redactor-convertable-' + tag).each(function(node)
9359        {
9360            var $el = this.utils.replaceToTag(node, tag);
9361            $el.removeClass('redactor-convertable-' + tag);
9362            if (this.utils.removeEmptyAttr($el, 'class')) $el.removeAttr('class');
9363
9364        }.bind(this));
9365    },
9366    _convertToStrike: function(inlines)
9367    {
9368        var selected = this.selection.getText().replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
9369
9370        for (var i = 0; i < inlines.length; i++)
9371        {
9372            var tag = this.arrangeTag(inlines[i].tagName.toLowerCase());
9373            var inline = inlines[i];
9374            var $inline = $R.dom(inline);
9375            var hasSameArgs = this.hasSameArgs(inline);
9376
9377            if (tag === this.tag)
9378            {
9379
9380                if (this.tag === 'span' && this._isTextSelected(inline, selected))
9381                {
9382                    $inline.addClass('redactor-convertable-apply');
9383                }
9384                else if (hasSameArgs && this.tag !== 'a')
9385                {
9386                    this._replaceToStrike($inline);
9387                }
9388                else if (this.tag === 'span')
9389                {
9390                    if (this.args && this.args.style) {
9391                        for (var key in this.args.style) {
9392                            $inline.css(key, '');
9393                        }
9394                    }
9395
9396                    if (this.utils.removeEmptyAttr($inline.get(), 'style')) {
9397                        $inline.addClass('redactor-convertable-apply');
9398                    }
9399                    else {
9400                        $inline.addClass('redactor-unconvertable-apply');
9401                    }
9402                }
9403                else if (!hasSameArgs)
9404                {
9405                    $inline.addClass('redactor-convertable-apply');
9406                }
9407
9408            }
9409        }
9410    },
9411    _replaceToStrike: function($el)
9412    {
9413        $el.replaceWith(function()
9414        {
9415            return $R.dom('<strike>').append($el.contents());
9416        });
9417    },
9418    _revertToInlines: function()
9419    {
9420        var nodes = [];
9421        var $editor = this.editor.getElement();
9422
9423        if (this.tag !== 'u') $editor.find('u').unwrap();
9424
9425        $editor.find('.redactor-convertable-u').each(function(node)
9426        {
9427             nodes.push(node);
9428
9429        });
9430
9431        // span convertable
9432        $editor.find('.redactor-convertable-apply').each(function(node)
9433        {
9434            var $node = $R.dom(node);
9435            $node.find('strike').unwrap();
9436
9437            this._forceRemoveClass($node, 'redactor-convertable-apply');
9438            nodes.push(node);
9439
9440        }.bind(this));
9441
9442        // span unconvertable
9443        $editor.find('span.redactor-unconvertable-apply').each(function(node)
9444        {
9445            var $node = $R.dom(node);
9446            this._forceRemoveClass($node, 'redactor-unconvertable-apply');
9447
9448        }.bind(this));
9449
9450        // strike
9451        $editor.find('strike').each(function(node)
9452        {
9453            var $node = this.utils.replaceToTag(node, this.tag);
9454            nodes.push($node.get());
9455
9456        }.bind(this));
9457
9458
9459        this._revertTags('u');
9460        this._revertTags('del');
9461
9462        return nodes;
9463    },
9464    _normalizeBlocks: function(nodes)
9465    {
9466        var tags = this.opts.inlineTags;
9467        var blocks = this.selection.getBlocks();
9468        if (blocks)
9469        {
9470            for (var i = 0; i < blocks.length; i++)
9471            {
9472                if (blocks[i].tagName === 'PRE')
9473                {
9474                    var $node = $R.dom(blocks[i]);
9475                    $node.find(tags.join(',')).not('.redactor-selection-marker').each(function(inline)
9476                    {
9477                        if (nodes.indexOf(inline) !== -1)
9478                        {
9479                            nodes = this.utils.removeFromArrayByValue(nodes, inline);
9480                        }
9481
9482                        $R.dom(inline).unwrap();
9483                    }.bind(this));
9484                }
9485            }
9486        }
9487
9488        return nodes;
9489    },
9490    _clearDecoration: function()
9491    {
9492        var $editor = this.editor.getElement();
9493        $editor.find(this.opts.inlineTags.join(',')).each(function(node)
9494        {
9495            if (node.style.textDecoration === 'line-through' || node.style.textDecorationLine === 'line-through')
9496            {
9497                var $el = $R.dom(node);
9498                $el.css('textDecorationLine', '');
9499                $el.css('textDecoration', '');
9500                $el.wrap('<strike>');
9501            }
9502        });
9503    },
9504    _clearEmptyStyle: function()
9505    {
9506        var inlines = this.getInlines();
9507        for (var i = 0; i < inlines.length; i++)
9508        {
9509            this._clearEmptyStyleAttr(inlines[i]);
9510
9511            var childNodes = inlines[i].childNodes;
9512            if (childNodes)
9513            {
9514                for (var z = 0; z < childNodes.length; z++)
9515                {
9516                    this._clearEmptyStyleAttr(childNodes[z]);
9517                }
9518            }
9519        }
9520    },
9521    _clearEmptyStyleAttr: function(node)
9522    {
9523        if (node.nodeType !== 3 && this.utils.removeEmptyAttr(node, 'style'))
9524        {
9525            node.removeAttribute('style');
9526            node.removeAttribute('data-redactor-style-cache');
9527        }
9528    },
9529    _forceRemoveClass: function($node, classname)
9530    {
9531        $node.removeClass(classname);
9532        if (this.utils.removeEmptyAttr($node, 'class')) $node.removeAttr('class');
9533    },
9534    _isTextSelected: function(node, selected)
9535    {
9536        var text = this.utils.removeInvisibleChars(node.textContent);
9537
9538        return (selected === text || selected.search(new RegExp('^' + this.utils.escapeRegExp(text) + '$')) !== -1);
9539    },
9540    getInlines: function(tags)
9541    {
9542        return (tags) ? this.selection.getInlines({ tags: tags, all: true }) : this.selection.getInlines({ all: true });
9543    },
9544    getElements: function(tags)
9545    {
9546        return $R.dom(this.getInlines(tags));
9547    },
9548    clearFormat: function()
9549    {
9550        this.selection.save();
9551
9552        var nodes = this.selection.getInlines({ all: true });
9553        for (var i = 0; i < nodes.length; i++)
9554        {
9555            var $el = $R.dom(nodes[i]);
9556            var inline = this.selection.getInline(nodes[i]);
9557            if (inline)
9558            {
9559                $el.unwrap();
9560            }
9561        }
9562
9563        this.selection.restore();
9564    }
9565});
9566$R.add('service', 'autoparser', {
9567    init: function(app)
9568    {
9569        this.app = app;
9570        this.cleaner = this.app.cleaner;
9571    },
9572    observe: function()
9573    {
9574        var $editor = this.editor.getElement();
9575        var $objects = $editor.find('.redactor-autoparser-object').each(function(node)
9576        {
9577           var $node = $R.dom(node);
9578           $node.removeClass('redactor-autoparser-object');
9579           if ($node.attr('class') === '') $node.removeAttr('class');
9580        });
9581
9582        if ($objects.length > 0)
9583        {
9584            $objects.each(function(node)
9585            {
9586                var type;
9587                var $object = false;
9588                var tag = node.tagName;
9589
9590                if (tag === 'A') type = 'link';
9591                else if (tag === 'IMG') type = 'image';
9592                else if (tag === 'IFRAME') type = 'video';
9593
9594                if (type)
9595                {
9596                    $object = $R.create(type + '.component', this.app, node);
9597                    this.app.broadcast(type + '.inserted', $object);
9598                    this.app.broadcast('autoparse', type, $object);
9599                }
9600
9601            }.bind(this));
9602        }
9603    },
9604    format: function(e, key)
9605    {
9606        if (this._isKey(key))
9607        {
9608            this._format(key === this.keycodes.ENTER);
9609        }
9610    },
9611    parse: function(html)
9612    {
9613        var tags = ['figure', 'html', 'form', 'pre', 'object', 'video', 'iframe', 'code', 'a', 'img'];
9614        var stored = [];
9615        var z = 0;
9616
9617        // store
9618        var storedComments = [];
9619        html = this.cleaner.storeComments(html, storedComments);
9620
9621        // encode
9622        html = this.cleaner.encodeCode(html);
9623
9624        // converting entity
9625        html = html.replace(/\$/g, '&#36;');
9626        html = html.replace(/&amp;/g, '&');
9627
9628        // store tags
9629        for (var i = 0; i < tags.length; i++)
9630        {
9631            var reTags = (tags[i] === 'img') ? '<' + tags[i] + '[^>]*>' : '<' + tags[i] + '([\\w\\W]*?)</' + tags[i] + '>';
9632            var matched = html.match(new RegExp(reTags, 'gi'));
9633
9634            if (matched !== null)
9635            {
9636                for (var y = 0; y < matched.length; y++)
9637                {
9638                    html = html.replace(matched[y], '#####replaceparse' + z + '#####');
9639                    stored.push(matched[y]);
9640                    z++;
9641                }
9642            }
9643        }
9644
9645        // images
9646        if (this.opts.autoparseImages && html.match(this.opts.regex.imageurl))
9647        {
9648            var imagesMatches = html.match(this.opts.regex.imageurl);
9649            for (var i = 0; i < imagesMatches.length; i++)
9650            {
9651                html = html.replace(imagesMatches[i], '<img class="redactor-autoparser-object" src="' + imagesMatches[i] + '">');
9652            }
9653        }
9654
9655        // video
9656        if (this.opts.autoparseVideo && (html.match(this.opts.regex.youtube) || html.match(this.opts.regex.vimeo)))
9657        {
9658            var iframeStart = '<iframe width="500" height="281" src="';
9659            var iframeEnd = '" frameborder="0" allowfullscreen></iframe>';
9660
9661            var str, re;
9662            var https = (this.opts.autoparseHttps) ? 'https:' : '';
9663            if (html.match(this.opts.regex.youtube))
9664            {
9665                str = https + '//www.youtube.com/embed/$1';
9666                re = this.opts.regex.youtube;
9667            }
9668            else if (html.match(this.opts.regex.vimeo))
9669            {
9670                str = https + '//player.vimeo.com/video/$2';
9671                re = this.opts.regex.vimeo;
9672            }
9673
9674            var $video = this.component.create('video', iframeStart + str + iframeEnd);
9675
9676            html = html.replace(re, $video.get().outerHTML);
9677        }
9678
9679        // store tags
9680        for (var i = 0; i < tags.length; i++)
9681        {
9682            var reTags = (tags[i] === 'img') ? '<' + tags[i] + '[^>]*>' : '<' + tags[i] + '([\\w\\W]*?)</' + tags[i] + '>';
9683            var matched = html.match(new RegExp(reTags, 'gi'));
9684
9685            if (matched !== null)
9686            {
9687                for (var y = 0; y < matched.length; y++)
9688                {
9689                    html = html.replace(matched[y], '#####replaceparse' + z + '#####');
9690                    stored.push(matched[y]);
9691                    z++;
9692                }
9693            }
9694        }
9695
9696        // links
9697        if (this.opts.autoparseLinks && html.match(this.opts.regex.url))
9698        {
9699            html = this._formatLinks(html);
9700        }
9701
9702        // restore
9703        html = this._restoreReplaced(stored, html);
9704
9705        // repeat for nested tags
9706        html = this._restoreReplaced(stored, html);
9707
9708        // restore comments
9709        html = this.cleaner.restoreComments(html, storedComments);
9710
9711        return html;
9712    },
9713
9714    // private
9715    _isKey: function(key)
9716    {
9717        return (key === this.keycodes.ENTER || key === this.keycodes.SPACE);
9718    },
9719    _format: function(enter)
9720    {
9721        var parent = this.selection.getParent();
9722        var $parent = $R.dom(parent);
9723
9724        var isNotFormatted = (parent && $parent.closest('figure, pre, code, img, a, iframe').length !== 0);
9725        if (isNotFormatted || !this.selection.isCollapsed())
9726        {
9727            return;
9728        }
9729
9730        // add split marker
9731        var marker = this.utils.createInvisibleChar();
9732        var range = this.selection.getRange();
9733        range.insertNode(marker);
9734
9735        var current = this.selection.getCurrent();
9736        var data = this.inspector.parse(current);
9737        var $current = $R.dom(current);
9738
9739        // remove split marker
9740        marker.parentNode.removeChild(marker);
9741
9742        if (current && current.nodeType === 3)
9743        {
9744            var content = current.textContent;
9745            var type;
9746
9747            // images
9748            if (this.opts.autoparseImages && content.match(this._convertToRegExp(this.opts.regex.imageurl)))
9749            {
9750                var isList = data.isList();
9751                var matches = content.match(this.opts.regex.imageurl);
9752                var el = (isList) ? undefined : '<figure><img></figure>';
9753
9754                var $img = this.component.create('image', el);
9755                $img.setSrc(matches[0]);
9756                $img.addClass('redactor-autoparser-object');
9757
9758                content = content.replace(matches[0], $img.get().outerHTML);
9759                type = 'image';
9760            }
9761            // video
9762            else if (this.opts.autoparseVideo && (content.match(this._convertToRegExp(this.opts.regex.youtube)) || content.match(this._convertToRegExp(this.opts.regex.vimeo))))
9763            {
9764                var iframeStart = '<iframe width="500" height="281" src="';
9765                var iframeEnd = '" frameborder="0" allowfullscreen></iframe>';
9766                var str, re;
9767
9768                if (content.match(this.opts.regex.youtube))
9769                {
9770                    str = '//www.youtube.com/embed/$1';
9771                    re = this.opts.regex.youtube;
9772                }
9773                else if (content.match(this.opts.regex.vimeo))
9774                {
9775                    str = '//player.vimeo.com/video/$2';
9776                    re = this.opts.regex.vimeo;
9777                }
9778
9779                var $video = this.component.create('video', iframeStart + str + iframeEnd);
9780                $video.addClass('redactor-autoparser-object');
9781
9782                content = content.replace(re, $video.get().outerHTML);
9783                type = 'video';
9784            }
9785            // links
9786            else if (this.opts.autoparseLinks && content.match(this._convertToRegExp(this.opts.regex.url)))
9787            {
9788                content = this._formatLinks(content, enter);
9789                type = 'link';
9790            }
9791
9792            // replace
9793            if (type)
9794            {
9795                if (enter)
9796                {
9797                    this.selection.save();
9798                    $current.replaceWith(content);
9799                    this.selection.restore();
9800                }
9801                else
9802                {
9803                    $current.replaceWith(content);
9804                }
9805
9806                // object
9807                var $editor = this.editor.getElement();
9808                var $object = $editor.find('.redactor-autoparser-object').removeClass('redactor-autoparser-object');
9809                $object = (type === 'link') ? $R.create('link.component', this.app, $object) : $object;
9810
9811                // caret
9812                if (type === 'link')
9813                {
9814                    if (!enter) this.caret.setAfter($object);
9815                    this.app.broadcast('link.inserted', $object);
9816                }
9817                else
9818                {
9819                    this.caret.setAfter($object);
9820
9821                    var $cloned = $object.clone();
9822                    $object.remove();
9823                    $object = this.insertion.insertHtml($cloned);
9824                    $object = this.component.build($object);
9825                }
9826
9827                // callback
9828                this.app.broadcast('autoparse', type, $object);
9829            }
9830        }
9831    },
9832    _formatLinks: function(content) {
9833        var target = (this.opts.pasteLinkTarget !== false) ? ' target="' + this.opts.pasteLinkTarget + '"' : '';
9834        var protocol = 'http';
9835        var classstr = ' class="redactor-autoparser-object"';
9836        var self = this;
9837
9838        content = content.replace(this.opts.regex.aurl1, function(url) {
9839            return '<a href="' + url + '"' + target + classstr + '>' + self._subLinkText(url) + '</a>';
9840        });
9841
9842        content = content.replace(this.opts.regex.aurl2, function(match, before, url) {
9843            return before + '<a href="' + protocol + '://' + url + '"' + target + classstr + '>' + self._subLinkText(url) + '</a>';
9844        });
9845
9846        return content;
9847    },
9848    _subLinkText: function(text) {
9849        text = (text.length > this.opts.linkSize) ? text.substring(0, this.opts.linkSize) + '...' : text;
9850        text = (text.search('%') === -1) ? decodeURIComponent(text) : text;
9851
9852        return text;
9853    },
9854    _restoreReplaced: function(stored, html) {
9855        for (var i = 0; i < stored.length; i++) {
9856            html = html.replace('#####replaceparse' + i + '#####', stored[i]);
9857        }
9858
9859        return html;
9860    },
9861    _convertToRegExp: function(str) {
9862        return new RegExp(String(str).replace(/^\//, '').replace(/\/ig$/, '').replace(/\/gi$/, '') + '$', 'gi');
9863    }
9864});
9865$R.add('service', 'storage', {
9866    init: function(app)
9867    {
9868        this.app = app;
9869
9870        // local
9871        this.data = [];
9872    },
9873    // public
9874    observeImages: function()
9875    {
9876        if (this.opts.imageObserve) {
9877            var $editor = this.editor.getElement();
9878            var $images = $editor.find('[data-image]');
9879            $images.each(this._addImage.bind(this));
9880        }
9881    },
9882    observeFiles: function()
9883    {
9884        var $editor = this.editor.getElement();
9885        var $files = $editor.find('[data-file]');
9886
9887		$files.each(this._addFile.bind(this));
9888    },
9889	setStatus: function(url, status)
9890	{
9891		this.data[url].status = status;
9892	},
9893    getChanges: function()
9894    {
9895        var $editor = this.editor.getElement();
9896
9897        // check status
9898        for (var key in this.data)
9899		{
9900			var data = this.data[key];
9901			var $el = $editor.find('[data-' + data.type + '="' + data.id + '"]');
9902
9903			this.setStatus(data.id, ($el.length === 0) ? false : true);
9904		}
9905
9906        return this.data;
9907    },
9908	add: function(type, node)
9909	{
9910        var $node = $R.dom(node);
9911        var id = $node.attr('data-' + type);
9912
9913        this.data[id] = { type: type, status: true, node: $node.get(), id: $node.attr('data-' + type) };
9914	},
9915
9916    // private
9917    _addImage: function(node)
9918    {
9919        this.add('image', node);
9920    },
9921    _addFile: function(node)
9922    {
9923        this.add('file', node);
9924    }
9925});
9926$R.add('service', 'utils', {
9927    init: function(app)
9928    {
9929        this.app = app;
9930    },
9931    // empty
9932    isEmpty: function(el)
9933    {
9934        var isEmpty = false;
9935        el = $R.dom(el).get();
9936        if (el)
9937        {
9938            isEmpty = (el.nodeType === 3) ? (el.textContent.trim().replace(/\n/, '') === '') : (el.innerHTML === '');
9939        }
9940
9941        return isEmpty;
9942    },
9943    isEmptyHtml: function(html, keepbr, keeplists)
9944    {
9945        html = this.removeInvisibleChars(html);
9946        html = html.replace(/&nbsp;/gi, '');
9947        html = html.replace(/<\/?br\s?\/?>/g, ((keepbr) ? 'br' : ''));
9948        html = html.replace(/\s/g, '');
9949        html = html.replace(/^<p>[^\W\w\D\d]*?<\/p>$/i, '');
9950        html = html.replace(/^<div>[^\W\w\D\d]*?<\/div>$/i, '');
9951
9952        if (keeplists)
9953        {
9954            html = html.replace(/<ul(.*?[^>])>$/i, 'ul');
9955            html = html.replace(/<ol(.*?[^>])>$/i, 'ol');
9956        }
9957
9958        html = html.replace(/<hr(.*?[^>])>$/i, 'hr');
9959        html = html.replace(/<iframe(.*?[^>])>$/i, 'iframe');
9960        html = html.replace(/<source(.*?[^>])>$/i, 'source');
9961
9962        // remove empty tags
9963        html = html.replace(/<[^\/>][^>]*><\/[^>]+>/gi, '');
9964        html = html.replace(/<[^\/>][^>]*><\/[^>]+>/gi, '');
9965
9966        // trim
9967        html = html.trim();
9968
9969        return html === '';
9970    },
9971    trimSpaces: function(html)
9972    {
9973        return html = this.removeInvisibleChars(html.trim());
9974    },
9975
9976    // invisible chars
9977    createInvisibleChar: function()
9978    {
9979        return document.createTextNode(this.opts.markerChar);
9980    },
9981    searchInvisibleChars: function(str)
9982    {
9983        return str.search(/^\uFEFF$/g);
9984    },
9985    removeInvisibleChars: function(html)
9986    {
9987        return html.replace(/\uFEFF/g, '');
9988    },
9989    trimInvisibleChars: function(direction)
9990    {
9991        if (!this.selection.isCollapsed()) return;
9992
9993        var current = this.selection.getCurrent();
9994        var side = (direction === 'left') ? this.selection.getTextBeforeCaret() : this.selection.getTextAfterCaret();
9995        var isSpace = (current && current.nodeType === 3 && this.searchInvisibleChars(side) === 0);
9996
9997        if (isSpace)
9998        {
9999            if (direction === 'left')
10000            {
10001                $R.dom(current).replaceWith(current.textContent.trim());
10002            }
10003            else
10004            {
10005                var offset = this.offset.get();
10006                this.offset.set({ start: offset.start + 1, end: offset.end + 1 });
10007            }
10008        }
10009    },
10010
10011    // wrapper
10012    buildWrapper: function(html)
10013    {
10014        return $R.dom('<div>').html(html);
10015    },
10016    getWrapperHtml: function($wrapper)
10017    {
10018        var html = $wrapper.html();
10019        $wrapper.remove();
10020
10021        return html;
10022    },
10023
10024    // fragment
10025    createTmpContainer: function(html)
10026    {
10027        var $div = $R.dom('<div>');
10028
10029        if (typeof html === 'string')
10030        {
10031            $div.html(html);
10032        }
10033        else
10034        {
10035            $div.append($R.dom(html).clone(true));
10036        }
10037
10038        return $div.get();
10039    },
10040    createFragment: function(html)
10041    {
10042        var el = this.createTmpContainer(html);
10043        var frag = document.createDocumentFragment(), node, firstNode, lastNode;
10044        var nodes = [];
10045        var i = 0;
10046        while ((node = el.firstChild))
10047        {
10048            i++;
10049            var n = frag.appendChild(node);
10050            if (i === 1) firstNode = n;
10051
10052            nodes.push(n);
10053            lastNode = n;
10054        }
10055
10056        return { frag: frag, first: firstNode, last: lastNode, nodes: nodes };
10057    },
10058    isFragment: function(obj)
10059    {
10060        return (typeof obj === 'object' && obj.frag);
10061    },
10062    parseHtml: function(html)
10063    {
10064        var div = this.createTmpContainer(html);
10065
10066        return { html: div.innerHTML, nodes: div.childNodes };
10067    },
10068
10069    splitNode: function(current, nodes, isList, inline)
10070    {
10071        nodes = (this.isFragment(nodes)) ? nodes.frag : nodes;
10072
10073        var element;
10074        if (inline)
10075        {
10076            element = (this.inspector.isInlineTag(current.tagName)) ? current : this.selection.getInline(current);
10077        }
10078        else
10079        {
10080            element = (this.inspector.isBlockTag(current.tagName)) ? current : this.selection.getBlock(current);
10081        }
10082
10083        var $element = $R.dom(element);
10084
10085        // replace is empty
10086        if (!inline && this.isEmptyHtml(element.innerHTML, true))
10087        {
10088            $element.after(nodes);
10089            $element.remove();
10090
10091            return nodes;
10092        }
10093
10094        var tag = $element.get().tagName.toLowerCase();
10095        var isEnd = this.caret.isEnd(element);
10096        var isStart = this.caret.isStart(element);
10097
10098        if (!isEnd && !isStart)
10099        {
10100            var extractedContent = this.extractHtmlFromCaret(inline);
10101
10102            var $secondPart = $R.dom('<' + tag + ' />');
10103            $secondPart = this.cloneAttributes(element, $secondPart);
10104
10105            $element.after($secondPart.append(extractedContent));
10106        }
10107
10108        if (isStart)
10109        {
10110            return $element.before(nodes);
10111        }
10112        else
10113        {
10114            if (isList)
10115            {
10116                return $element.append(nodes);
10117            }
10118            else
10119            {
10120                nodes = $element.after(nodes);
10121
10122                var html = $element.html();
10123                html = this.removeInvisibleChars(html);
10124                html = html.replace(/&nbsp;/gi, '');
10125
10126                if (html === '') $element.remove();
10127
10128                return nodes;
10129            }
10130        }
10131    },
10132    extractHtmlFromCaret: function(inline, element)
10133    {
10134        var range = this.selection.getRange();
10135        if (range)
10136        {
10137            element = (element) ? element : ((inline) ? this.selection.getInline() : this.selection.getBlock());
10138            if (element)
10139            {
10140                var clonedRange = range.cloneRange();
10141                clonedRange.selectNodeContents(element);
10142                clonedRange.setStart(range.endContainer, range.endOffset);
10143
10144                return clonedRange.extractContents();
10145            }
10146        }
10147    },
10148    createMarkup: function(el)
10149    {
10150        var markup = document.createElement(this.opts.markup);
10151        if (this.opts.breakline) markup.setAttribute('data-redactor-tag', 'br');
10152
10153        var $el = $R.dom(el);
10154
10155        $el.after(markup);
10156        this.caret.setStart(markup);
10157    },
10158    createMarkupBefore: function(el)
10159    {
10160        var markup = document.createElement(this.opts.markup);
10161        if (this.opts.breakline) markup.setAttribute('data-redactor-tag', 'br');
10162
10163        var $el = $R.dom(el);
10164
10165        $el.before(markup);
10166        this.caret.setEnd(markup);
10167    },
10168    getNode: function(el)
10169    {
10170        var node = $R.dom(el).get();
10171        var editor = this.editor.getElement().get();
10172
10173        return (typeof el === 'undefined') ? editor : ((node) ? node : false);
10174    },
10175    findSiblings: function(node, type)
10176    {
10177        node = $R.dom(node).get();
10178        type = (type === 'next') ? 'nextSibling' : 'previousSibling';
10179
10180        while (node = node[type])
10181        {
10182            if ((node.nodeType === 3 && node.textContent.trim() === '') || node.tagName === 'BR')
10183            {
10184                continue;
10185            }
10186
10187            return node;
10188        }
10189
10190        return false;
10191    },
10192    getElementsFromHtml: function(html, selector, exclude)
10193    {
10194        var div = document.createElement("div");
10195        div.innerHTML = html;
10196
10197        var elems = div.querySelectorAll(selector);
10198
10199        // array map polyfill
10200        var mapping = function(callback, thisArg)
10201        {
10202            if (typeof this.length !== 'number') return;
10203            if (typeof callback !== 'function') return;
10204
10205            var newArr = [];
10206            if (typeof this == 'object')
10207            {
10208                for (var i = 0; i < this.length; i++)
10209                {
10210                    if (i in this) newArr[i] = callback.call(thisArg || this, this[i], i, this);
10211                    else return;
10212                }
10213            }
10214
10215            return newArr;
10216        };
10217
10218        return mapping.call(elems, function(el)
10219        {
10220            var type = el.getAttribute('data-redactor-type');
10221            if (exclude && type && type === exclude) {}
10222            else return el.outerHTML;
10223        });
10224    },
10225
10226    // childnodes
10227    getChildNodes: function(el, recursive, elements)
10228    {
10229        el = (el && el.nodeType && el.nodeType === 11) ? el : $R.dom(el).get();
10230
10231        var nodes = el.childNodes;
10232        var result = [];
10233        if (nodes)
10234        {
10235            for (var i = 0; i < nodes.length; i++)
10236            {
10237                if (elements === true && nodes[i].nodeType === 3) continue;
10238                else if (nodes[i].nodeType === 3 && this.isEmpty(nodes[i])) continue;
10239
10240                result.push(nodes[i]);
10241
10242                if (recursive !== false)
10243                {
10244                    var nestedNodes = this.getChildNodes(nodes[i], elements);
10245                    if (nestedNodes.length > 0)
10246                    {
10247                        result = result.concat(nestedNodes);
10248                    }
10249                }
10250            }
10251        }
10252
10253        return result;
10254    },
10255    getChildElements: function(el)
10256    {
10257        return this.getChildNodes(el, true, true);
10258    },
10259    getFirstNode: function(el)
10260    {
10261        return this._getFirst(this.getChildNodes(el, false));
10262    },
10263    getLastNode: function(el)
10264    {
10265        return this._getLast(this.getChildNodes(el, false));
10266    },
10267    getFirstElement: function(el)
10268    {
10269        return this._getFirst(this.getChildNodes(el, false, true));
10270    },
10271    getLastElement: function(el)
10272    {
10273        return this._getLast(this.getChildNodes(el, false, true));
10274    },
10275
10276    // replace
10277    replaceToTag: function(node, tag)
10278    {
10279        var $node = $R.dom(node);
10280        return $node.replaceWith(function(node)
10281        {
10282            var $replaced = $R.dom('<' + tag + '>').append($R.dom(node).contents());
10283            if (node.attributes)
10284            {
10285                var attrs = node.attributes;
10286                for (var i = 0; i < attrs.length; i++)
10287                {
10288                    $replaced.attr(attrs[i].nodeName, attrs[i].value);
10289                }
10290            }
10291
10292            return $replaced;
10293
10294        });
10295    },
10296
10297    // string
10298    ucfirst: function(str)
10299    {
10300        return str.charAt(0).toUpperCase() + str.slice(1);
10301    },
10302
10303    // array
10304    removeFromArrayByValue: function(arr, value)
10305    {
10306        var a = arguments, len = a.length, ax;
10307        while (len > 1 && arr.length)
10308        {
10309            value = a[--len];
10310            while ((ax= arr.indexOf(value)) !== -1)
10311            {
10312                arr.splice(ax, 1);
10313            }
10314        }
10315
10316        return arr;
10317    },
10318
10319    // attributes
10320    removeEmptyAttr: function(el, attr)
10321    {
10322        var $el = $R.dom(el);
10323
10324        if (typeof $el.attr(attr) === 'undefined' || $el.attr(attr) === null) return true;
10325        else if ($el.attr(attr) === '')
10326        {
10327            $el.removeAttr(attr);
10328            return true;
10329        }
10330
10331        return false;
10332    },
10333    cloneAttributes: function(elFrom, elTo)
10334    {
10335        elFrom = $R.dom(elFrom).get();
10336        elTo = $R.dom(elTo);
10337
10338        var attrs = elFrom.attributes;
10339        var len = attrs.length;
10340        while (len--)
10341        {
10342            var attr = attrs[len];
10343            elTo.attr(attr.name, attr.value);
10344        }
10345
10346        return elTo;
10347    },
10348
10349    // object
10350    toParams: function(obj)
10351    {
10352        if (typeof obj !== 'object') return obj;
10353
10354        var keys = Object.keys(obj);
10355        if (!keys.length) return '';
10356        var result = '';
10357
10358        for (var i = 0; i < keys.length; i++)
10359        {
10360            var key = keys[i];
10361            result += key + ':' + obj[key] + ';';
10362        }
10363
10364        return result;
10365    },
10366    styleToObj: function(str)
10367    {
10368        var obj = {};
10369
10370        if (str)
10371        {
10372            var style = str.replace(/;$/, '').split(';');
10373            for (var i = 0; i < style.length; i++)
10374            {
10375                var rule = style[i].split(':');
10376                obj[rule[0].trim()] = rule[1].trim();
10377            }
10378        }
10379
10380        return obj;
10381    },
10382    checkProperty: function(obj)
10383    {
10384        var args = (arguments[1] && Array.isArray(arguments[1])) ? arguments[1] : [].slice.call(arguments, 1);
10385
10386        for (var i = 0; i < args.length; i++)
10387        {
10388            if (!obj || (typeof obj[args[i]] === 'undefined'))
10389            {
10390                return false;
10391            }
10392
10393            obj = obj[args[i]];
10394        }
10395
10396        return obj;
10397    },
10398
10399    // data
10400    extendData: function(data, obj)
10401    {
10402        for (var key in obj)
10403        {
10404            if (key === 'elements')
10405            {
10406                var $elms = $R.dom(obj[key]);
10407                $elms.each(function(node)
10408                {
10409                    var $node = $R.dom(node);
10410                    if (node.tagName === 'FORM')
10411                    {
10412                        var serializedData = $node.serialize(true);
10413                        for (var z in serializedData)
10414                        {
10415                            data = this._setData(data, z, serializedData[z]);
10416                        }
10417                    }
10418                    else
10419                    {
10420                        var name = ($node.attr('name')) ? $node.attr('name') : $node.attr('id');
10421                        data = this._setData(data, name, $node.val());
10422                    }
10423                }.bind(this));
10424            }
10425            else
10426            {
10427                data = this._setData(data, key, obj[key]);
10428            }
10429        }
10430
10431        return data;
10432    },
10433    _setData: function(data, name, value)
10434    {
10435        if (data instanceof FormData) data.append(name, value);
10436        else data[name] = value;
10437
10438        return data;
10439    },
10440
10441    // normalize
10442    normalizeTextNodes: function(el)
10443    {
10444        el = $R.dom(el).get();
10445        if (el) el.normalize();
10446    },
10447
10448    // color
10449    isRgb: function(str)
10450    {
10451        return (str.search(/^rgb/i) === 0);
10452    },
10453    rgb2hex: function(rgb)
10454    {
10455        rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
10456
10457        return (rgb && rgb.length === 4) ? "#" +
10458        ("0" + parseInt(rgb[1],10).toString(16)).slice(-2) +
10459        ("0" + parseInt(rgb[2],10).toString(16)).slice(-2) +
10460        ("0" + parseInt(rgb[3],10).toString(16)).slice(-2) : '';
10461    },
10462    hex2long: function(val)
10463    {
10464        if (val.search(/^#/) !== -1 && val.length === 4)
10465        {
10466            val = '#' + val[1] + val[1] + val[2] + val[2] + val[3] + val[3];
10467        }
10468
10469        return val;
10470    },
10471
10472    // escape
10473    escapeRegExp: function(s)
10474    {
10475        return s.replace(/[-\/\\^$*~+?.()|[\]{}]/g, '\\$&');
10476    },
10477
10478    // random
10479    getRandomId: function()
10480    {
10481        var id = '';
10482        var possible = 'abcdefghijklmnopqrstuvwxyz0123456789';
10483
10484        for (var i = 0; i < 12; i++)
10485        {
10486            id += possible.charAt(Math.floor(Math.random() * possible.length));
10487        }
10488
10489        return id;
10490    },
10491
10492    // private
10493    _getFirst: function(nodes)
10494    {
10495        return (nodes.length !== 0) ? nodes[0] : false;
10496    },
10497    _getLast: function(nodes)
10498    {
10499        return (nodes.length !== 0) ? nodes[nodes.length-1] : false;
10500    }
10501});
10502$R.add('service', 'progress', {
10503    init: function(app)
10504    {
10505        this.app = app;
10506
10507        // local
10508        this.$box = null;
10509        this.$bar = null;
10510    },
10511
10512    // public
10513    show: function()
10514    {
10515        if (!this._is()) this._build();
10516        this.$box.show();
10517    },
10518    hide: function()
10519    {
10520        if (this._is())
10521        {
10522            this.animate.start(this.$box, 'fadeOut', this._destroy.bind(this));
10523        }
10524    },
10525    update: function(value)
10526    {
10527        this.show();
10528        this.$bar.css('width', value + '%');
10529    },
10530
10531    // private
10532    _is: function()
10533    {
10534        return (this.$box !== null);
10535    },
10536    _build: function()
10537    {
10538        this.$bar = $R.dom('<span />');
10539        this.$box = $R.dom('<div id="redactor-progress" />');
10540
10541        this.$box.append(this.$bar);
10542        this.$body.append(this.$box);
10543    },
10544    _destroy: function()
10545    {
10546        if (this._is()) this.$box.remove();
10547
10548        this.$box = null;
10549        this.$bar = null;
10550    }
10551});
10552$R.add('module', 'starter', {
10553    init: function(app)
10554    {
10555        this.app = app;
10556        this.opts = app.opts;
10557        this.plugin = app.plugin;
10558        this.module = app.module;
10559    },
10560    // messages
10561    onstart: function()
10562    {
10563        var services = ['element', 'container', 'source', 'editor', 'statusbar', 'toolbar'];
10564        var modules = ['element', 'container', 'source', 'editor', 'statusbar', 'contextbar', 'input'];
10565
10566        this._startStop('start', this.app, services);
10567        this._startStop('start', this.module, modules);
10568    },
10569    onstop: function()
10570    {
10571        var modules = ['observer', 'element', 'container', 'source', 'editor', 'contextbar'];
10572
10573        this._startStop('stop', this.module, modules);
10574    },
10575    onenable: function()
10576    {
10577        var modules = ['observer', 'toolbar'];
10578        var plugins = this.opts.plugins;
10579
10580        this._startStop('start', this.module, modules);
10581        this._startStop('start', this.plugin, plugins);
10582    },
10583    ondisable: function()
10584    {
10585        var modules = ['observer', 'toolbar'];
10586        var plugins = this.opts.plugins;
10587
10588        this._startStop('stop', this.module, modules);
10589        this._startStop('stop', this.plugin, plugins);
10590    },
10591
10592    // private
10593    _startStop: function(type, obj, arr)
10594    {
10595        for (var i = 0; i < arr.length; i++)
10596        {
10597            if (typeof obj[arr[i]] !== 'undefined')
10598            {
10599                this.app.callInstanceMethod(obj[arr[i]], type);
10600            }
10601        }
10602    }
10603});
10604$R.add('module', 'element', {
10605    init: function(app)
10606    {
10607        this.app = app;
10608        this.uuid = app.uuid;
10609        this.opts = app.opts;
10610        this.namespace = app.namespace;
10611        this.element = app.element;
10612        this.rootOpts = $R.extend({}, true, $R.options, app.rootOpts);
10613    },
10614    // public
10615    start: function()
10616    {
10617        this._build();
10618        this._buildModes();
10619        this._buildMarkup();
10620    },
10621    stop: function()
10622    {
10623        var $element = this.element.getElement();
10624        $element.removeData(this.namespace + '-uuid');
10625    },
10626
10627    // private
10628    _build: function()
10629    {
10630        var $element = this.element.getElement();
10631        $element.data(this.namespace + '-uuid', this.uuid);
10632    },
10633    _buildModes: function()
10634    {
10635        var type = this.element.getType();
10636
10637        if (type === 'inline') this._redefineOptions(this.opts.modes['inline']);
10638        if (type === 'div') this._redefineOptions(this.opts.modes['original']);
10639
10640        if (type !== 'inline')
10641        {
10642            if (this._isRootOption('styles') && this.rootOpts.styles) this.opts.styles = true;
10643            if (this._isRootOption('source') && !this.rootOpts.source) this.opts.showSource = false;
10644        }
10645    },
10646    _buildMarkup: function()
10647    {
10648        var type = this.element.getType();
10649
10650        if (type === 'inline')
10651        {
10652            this.opts.emptyHtml = '';
10653        }
10654        else if (this.opts.breakline)
10655        {
10656            this.opts.markup = 'div';
10657            this.opts.emptyHtml = '<div data-redactor-tag="br">' + this.opts.markerChar + '</div>';
10658        }
10659        else
10660        {
10661            this.opts.emptyHtml = '<' + this.opts.markup + '></' + this.opts.markup + '>';
10662        }
10663    },
10664    _redefineOptions: function(opts)
10665    {
10666        for (var key in opts)
10667        {
10668            this.opts[key] = opts[key];
10669        }
10670    },
10671    _isRootOption: function()
10672    {
10673        return (typeof this.rootOpts['styles'] !== 'undefined');
10674    }
10675});
10676$R.add('module', 'editor', {
10677    init: function(app)
10678    {
10679        this.app = app;
10680        this.uuid = app.uuid;
10681        this.opts = app.opts;
10682        this.editor = app.editor;
10683        this.source = app.source;
10684        this.element = app.element;
10685        this.component = app.component;
10686        this.container = app.container;
10687        this.inspector = app.inspector;
10688        this.autoparser = app.autoparser;
10689
10690        // local
10691        this.placeholder = false;
10692        this.events = false;
10693    },
10694    // messages
10695    onenable: function()
10696    {
10697        this.enable();
10698    },
10699    ondisable: function()
10700    {
10701        this.disable();
10702    },
10703    onenablefocus: function()
10704    {
10705        this._enableFocus();
10706    },
10707    oncontextmenu: function(e)
10708    {
10709        this.component.setOnEvent(e, true);
10710    },
10711    onclick: function(e)
10712    {
10713        this.component.setOnEvent(e);
10714    },
10715    onkeyup: function(e)
10716    {
10717        var data = this.inspector.parse(e.target);
10718        if (!data.isComponent())
10719        {
10720            this.component.clearActive();
10721        }
10722    },
10723    onenablereadonly: function()
10724    {
10725        this._enableReadOnly();
10726    },
10727    ondisablereadonly: function()
10728    {
10729        this._disableReadOnly();
10730    },
10731    onautoparseobserve: function()
10732    {
10733        this.autoparser.observe();
10734    },
10735    onplaceholder: {
10736        build: function()
10737        {
10738            this._buildPlaceholder();
10739        },
10740        toggle: function()
10741        {
10742            this._togglePlacehodler();
10743        }
10744    },
10745
10746    // public
10747    start: function()
10748    {
10749        this._build();
10750        this._buildEvents();
10751        this._buildOptions();
10752        this._buildAccesibility();
10753    },
10754    stop: function()
10755    {
10756        var $editor = this.editor.getElement();
10757        var $container = this.container.getElement();
10758
10759        var classesEditor = ['redactor-in', 'redactor-in-' + this.uuid, 'redactor-structure', 'redactor-placeholder', 'notranslate'];
10760        if (this.opts.stylesClass !== '') {
10761            classesEditor.push(this.opts.stylesClass);
10762        }
10763
10764        var classesContainer = ['redactor-focus', 'redactor-blur', 'redactor-over', 'redactor-styles-on',
10765                                'redactor-styles-off', 'redactor-toolbar-on', 'redactor-text-labeled-on', 'redactor-source-view'];
10766
10767        $editor.removeAttr('spellcheck');
10768        $editor.removeAttr('dir');
10769        $editor.removeAttr('contenteditable');
10770        $editor.removeAttr('placeholder');
10771        $editor.removeAttr('data-gramm_editor');
10772        $editor.removeClass(classesEditor.join(' '));
10773
10774        $container.removeClass(classesContainer.join(' '));
10775
10776        this._destroyEvents();
10777
10778        if ($editor.get().classList.length === 0) $editor.removeAttr('class');
10779    },
10780    enable: function()
10781    {
10782        var $editor = this.editor.getElement();
10783        var $container = this.container.getElement();
10784
10785        $editor.addClass('redactor-in redactor-in-' + this.uuid);
10786        $editor.attr({ 'contenteditable': true });
10787
10788        if (this.opts.structure)
10789        {
10790            $editor.addClass('redactor-structure');
10791        }
10792
10793        if (this.opts.toolbar && !this.opts.air && !this.opts.toolbarExternal)
10794        {
10795            $container.addClass('redactor-toolbar-on');
10796        }
10797
10798        // prevent editing
10799        this._disableBrowsersEditing();
10800    },
10801    disable: function()
10802    {
10803        var $editor = this.editor.getElement();
10804        var $container = this.container.getElement();
10805
10806        $editor.removeClass('redactor-in redactor-in-' + this.uuid);
10807        $editor.removeClass('redactor-structure');
10808        $editor.removeAttr('contenteditable');
10809
10810        $container.addClass('redactor-toolbar-on');
10811    },
10812
10813    // private
10814    _build: function()
10815    {
10816        var $editor = this.editor.getElement();
10817        var $element = this.element.getElement();
10818        var $container = this.container.getElement();
10819
10820        $container.addClass('redactor-blur');
10821
10822        if (!this.opts.grammarly)
10823        {
10824            $editor.attr('data-gramm_editor', false);
10825        }
10826
10827        if (this.opts.notranslate)
10828        {
10829            $editor.addClass('notranslate');
10830        }
10831
10832        if (this.opts.styles)
10833        {
10834            $editor.addClass(this.opts.stylesClass);
10835            $container.addClass('redactor-styles-on');
10836        }
10837        else
10838        {
10839            $container.addClass('redactor-styles-off');
10840        }
10841
10842        if (this.opts.buttonsTextLabeled)
10843        {
10844            $container.addClass('redactor-text-labeled-on');
10845        }
10846
10847        if (this.element.isType('textarea')) $element.before($editor);
10848    },
10849    _buildEvents: function()
10850    {
10851        this.events = $R.create('editor.events', this.app);
10852    },
10853    _buildOptions: function()
10854    {
10855        var $editor = this.editor.getElement();
10856
10857        $editor.attr('dir', this.opts.direction);
10858
10859        if (!this.opts.spellcheck) $editor.attr('spellcheck', false);
10860        if (this.opts.tabindex) $editor.attr('tabindex', this.opts.tabindex);
10861        if (this.opts.minHeight) $editor.css('min-height', this.opts.minHeight);
10862        if (this.opts.maxHeight) $editor.css('max-height', this.opts.maxHeight);
10863        if (this.opts.maxWidth) $editor.css({ 'max-width': this.opts.maxWidth, 'margin': 'auto' });
10864    },
10865    _buildAccesibility: function()
10866    {
10867        var $editor = this.editor.getElement();
10868
10869        $editor.attr({ 'aria-labelledby': 'redactor-voice-' + this.uuid, 'role': 'presentation' });
10870    },
10871    _buildPlaceholder: function()
10872    {
10873        this.placeholder = $R.create('editor.placeholder', this.app);
10874    },
10875    _enableFocus: function()
10876    {
10877        if (this.opts.showSource) this._enableFocusSource();
10878        else this._enableFocusEditor();
10879    },
10880    _enableFocusSource: function()
10881    {
10882        var $source = this.source.getElement();
10883
10884        if (this.opts.focus)
10885        {
10886            $source.focus();
10887            $source.get().setSelectionRange(0, 0);
10888        }
10889        else if (this.opts.focusEnd)
10890        {
10891            $source.focus();
10892        }
10893    },
10894    _enableFocusEditor: function()
10895    {
10896        if (this.opts.focus)
10897        {
10898            setTimeout(this.editor.startFocus.bind(this.editor), 100);
10899        }
10900        else if (this.opts.focusEnd)
10901        {
10902            setTimeout(this.editor.endFocus.bind(this.editor), 100);
10903        }
10904    },
10905    _togglePlacehodler: function()
10906    {
10907        if (this.placeholder) this.placeholder.toggle();
10908    },
10909    _disableBrowsersEditing: function()
10910    {
10911        try {
10912            // FF fix
10913            document.execCommand('enableObjectResizing', false, false);
10914            document.execCommand('enableInlineTableEditing', false, false);
10915            // IE prevent converting links
10916            document.execCommand("AutoUrlDetect", false, false);
10917
10918            // IE disable image resizing
10919            var $editor = this.editor.getElement();
10920            var el = $editor.get();
10921            if (el.addEventListener) el.addEventListener('mscontrolselect', function(e) { e.preventDefault(); });
10922            else el.attachEvent('oncontrolselect', function(e) { e.returnValue = false; });
10923
10924        } catch (e) {}
10925    },
10926    _destroyEvents: function()
10927    {
10928        if (this.events)
10929        {
10930            this.events.destroy();
10931        }
10932    },
10933    _enableReadOnly: function()
10934    {
10935        var $editor = this.editor.getElement();
10936
10937        this._getEditables($editor).removeAttr('contenteditable');
10938        $editor.removeAttr('contenteditable');
10939        $editor.addClass('redactor-read-only');
10940
10941        if (this.events) this.events.destroy();
10942    },
10943    _disableReadOnly: function()
10944    {
10945        var $editor = this.editor.getElement();
10946
10947        this._getEditables($editor).attr({ 'contenteditable': true });
10948        $editor.removeClass('redactor-read-only');
10949        $editor.attr({ 'contenteditable': true });
10950
10951        this._buildEvents();
10952    },
10953    _getEditables: function($editor)
10954    {
10955        return $editor.find('figcaption, td, th');
10956    }
10957});
10958$R.add('class', 'editor.placeholder', {
10959    init: function(app)
10960    {
10961        this.app = app;
10962        this.opts = app.opts;
10963        this.editor = app.editor;
10964        this.element = app.element;
10965
10966        // build
10967        this.build();
10968    },
10969    build: function()
10970    {
10971        var $element = this.element.getElement();
10972        var $editor = this.editor.getElement();
10973
10974        if (this.opts.placeholder !== false || $element.attr('placeholder'))
10975        {
10976            var text = (this.opts.placeholder !== false) ? this.opts.placeholder : $element.attr('placeholder');
10977            $editor.attr('placeholder', text);
10978            this.toggle();
10979        }
10980    },
10981    toggle: function()
10982    {
10983        return (this.editor.isEmpty(true)) ? this.show() : this.hide();
10984    },
10985    show: function()
10986    {
10987        var $editor = this.editor.getElement();
10988        $editor.addClass('redactor-placeholder');
10989    },
10990    hide: function()
10991    {
10992        var $editor = this.editor.getElement();
10993        $editor.removeClass('redactor-placeholder');
10994    }
10995});
10996$R.add('class', 'editor.events', {
10997    init: function(app)
10998    {
10999        this.app = app;
11000        this.opts = app.opts;
11001        this.$doc = app.$doc;
11002        this.uuid = app.uuid;
11003        this.source = app.source;
11004        this.editor = app.editor;
11005        this.cleaner = app.cleaner;
11006        this.container = app.container;
11007        this.insertion = app.insertion;
11008        this.inspector = app.inspector;
11009        this.selection = app.selection;
11010        this.component = app.component;
11011
11012        // local
11013        this.blurNamespace = '.redactor-blur.' + this.uuid;
11014        this.eventsList = ['paste', 'click', 'contextmenu', 'keydown', 'keyup', 'mouseup', 'touchstart',
11015                           'cut', 'copy', 'dragenter', 'dragstart', 'drop', 'dragover', 'dragleave'];
11016
11017        // init
11018        this._init();
11019    },
11020    destroy: function()
11021    {
11022        var $editor = this.editor.getElement();
11023
11024        $editor.off('.redactor-focus');
11025        this.$doc.off('keyup' + this.blurNamespace + ' mousedown' + this.blurNamespace);
11026
11027        // all events
11028        this._loop('off');
11029    },
11030    focus: function(e)
11031    {
11032        var $container = this.container.getElement();
11033
11034        if (this.editor.isPasting() || $container.hasClass('redactor-focus')) return;
11035
11036        $container.addClass('redactor-focus');
11037        $container.removeClass('redactor-blur');
11038
11039        this.app.broadcast('observe', e);
11040        this.app.broadcast('focus', e);
11041
11042        this.isFocused = true;
11043        this.isBlured = false;
11044    },
11045    blur: function(e)
11046    {
11047        var $container = this.container.getElement();
11048        var $target = $R.dom(e.target);
11049        var targets = ['.redactor-in-' + this.uuid,  '.redactor-toolbar', '.redactor-dropdown',
11050        '.redactor-context-toolbar', '.redactor-modal-box', '#redactor-image-resizer'];
11051
11052        this.app.broadcast('originalblur', e);
11053        if (this.app.stopBlur) return;
11054
11055        if (!this.app.isStarted() || this.editor.isPasting()) return;
11056        if ($target.closest(targets.join(',')).length !== 0) return;
11057
11058        if (!this.isBlured && !$container.hasClass('redactor-blur'))
11059        {
11060            $container.removeClass('redactor-focus');
11061            $container.addClass('redactor-blur');
11062            this.app.broadcast('blur', e);
11063
11064            this.isFocused = false;
11065            this.isBlured = true;
11066        }
11067    },
11068    cut: function(e)
11069    {
11070        var current = this.selection.getCurrent();
11071        var data = this.inspector.parse(current);
11072
11073        this.app.broadcast('state', e);
11074
11075        if (this.component.isNonEditable(current))
11076        {
11077            this._passSelectionToClipboard(e, data, true);
11078            e.preventDefault();
11079        }
11080    },
11081    copy: function(e)
11082    {
11083        var current = this.selection.getCurrent();
11084        var data = this.inspector.parse(current);
11085
11086        this.app.broadcast('state', e);
11087
11088        if (this.component.isNonEditable(current))
11089        {
11090            this._passSelectionToClipboard(e, data, false);
11091            e.preventDefault();
11092        }
11093    },
11094    drop: function(e)
11095    {
11096        e = e.originalEvent || e;
11097        e.stopPropagation();
11098        this._removeOverClass();
11099
11100        if (this.opts.dragUpload === false)
11101        {
11102            e.preventDefault();
11103            return;
11104        }
11105
11106
11107        if (this.app.isDragComponentInside())
11108        {
11109            var $dragComponent = $R.dom(this.app.getDragComponentInside());
11110            var $component = $dragComponent.clone(true);
11111            this.insertion.insertToPoint(e, $component);
11112
11113            $dragComponent.remove();
11114
11115            this.app.setDragComponentInside(false);
11116            this.app.broadcast('state', e);
11117            this.app.broadcast('drop', e);
11118            this.app.broadcast('image.observe', e);
11119
11120            e.preventDefault();
11121
11122            return;
11123        }
11124        else if (this.app.isDragInside() && this.opts.input)
11125        {
11126            this.insertion.insertPoint(e);
11127
11128            var dt = e.dataTransfer;
11129            var html = dt.getData('text/html');
11130
11131            // clear selected
11132            var range = this.selection.getRange();
11133            if (range)
11134            {
11135                var blocks = this.selection.getBlocks();
11136                range.deleteContents();
11137
11138                // remove empty blocks
11139                for (var i = 0; i < blocks.length; i++)
11140                {
11141                    if (blocks[i].innerHTML === '') $R.dom(blocks[i]).remove();
11142                }
11143            }
11144
11145            // paste
11146            $R.create('input.paste', this.app, e, true, html, true);
11147
11148            this.app.broadcast('state', e);
11149            this.app.broadcast('drop', e);
11150
11151            this.app.setDragInside(false);
11152            e.preventDefault();
11153
11154            return;
11155        }
11156
11157        this.app.broadcast('state', e);
11158        this.app.broadcast('paste', e, e.dataTransfer);
11159        this.app.broadcast('drop', e);
11160
11161    },
11162    dragenter: function(e)
11163    {
11164        e.preventDefault();
11165    },
11166    dragstart: function(e)
11167    {
11168        this.app.setDragComponentInside(false);
11169        this.app.setDragInside(false);
11170
11171        var data = this.inspector.parse(e.target);
11172        if (data.isComponent() && !data.isComponentEditable() && !data.isFigcaption())
11173        {
11174            this.app.setDragComponentInside(data.getComponent());
11175        }
11176        else if (this.selection.is() && !this.selection.isCollapsed())
11177        {
11178            // drag starts inside editor
11179            this.app.setDragInside(true);
11180            this._setDragData(e);
11181        }
11182
11183        this.app.broadcast('dragstart', e);
11184    },
11185    dragover: function(e)
11186    {
11187        this.app.broadcast('dragover', e);
11188    },
11189    dragleave: function(e)
11190    {
11191        this.app.broadcast('dragleave', e);
11192    },
11193    paste: function(e)
11194    {
11195        this.app.broadcast('paste', e);
11196    },
11197    contextmenu: function(e)
11198    {
11199        // chrome crashes fix
11200        //this.editor.disableNonEditables();
11201
11202        //setTimeout(function()
11203        //{
11204          //  this.editor.enableNonEditables();
11205           // this.app.broadcast('contextmenu', e);
11206
11207        //}.bind(this), 0);
11208    },
11209    click: function(e)
11210    {
11211        // triple click selection
11212        if (e.detail === 3)
11213        {
11214            e.preventDefault();
11215
11216            var block = this.selection.getBlock();
11217            if (block) {
11218                var range = document.createRange();
11219                range.selectNodeContents(block);
11220                this.selection.setRange(range)
11221            }
11222        }
11223
11224        // observe bottom click & start click & outside click
11225        var $target = $R.dom(e.target);
11226        if ($target.hasClass('redactor-in'))
11227        {
11228            var top = $target.offset().top;
11229            var pad = parseFloat($target.css('padding-bottom'));
11230            var height = $target.height();
11231            var posHeight = top + height - pad*2;
11232
11233            if (posHeight < e.pageY)
11234            {
11235                this.app.broadcast('bottomclick', e);
11236            }
11237            else if ($target.hasClass('redactor-placeholder')) {
11238                this.editor.startFocus(this.editor);
11239            }
11240        }
11241
11242        this.app.broadcast('state', e);
11243        this.app.broadcast('click', e);
11244    },
11245    keydown: function(e)
11246    {
11247        this.app.broadcast('state', e);
11248        var stop = this.app.broadcast('keydown', e);
11249        if (stop === false)
11250        {
11251            return e.preventDefault();
11252        }
11253    },
11254    keyup: function(e)
11255    {
11256        this.app.broadcast('keyup', e);
11257    },
11258    mouseup: function(e)
11259    {
11260        this.app.broadcast('observe', e);
11261        this.app.broadcast('state', e);
11262    },
11263    touchstart: function(e)
11264    {
11265        this.app.broadcast('observe', e);
11266        this.app.broadcast('state', e);
11267    },
11268
11269    // private
11270    _init: function()
11271    {
11272        var $editor = this.editor.getElement();
11273
11274        $editor.on('focus.redactor-focus click.redactor-focus', this.focus.bind(this));
11275        this.$doc.on('keyup' + this.blurNamespace + ' mousedown' + this.blurNamespace, this.blur.bind(this));
11276
11277        // all events
11278        this._loop('on');
11279    },
11280    _removeOverClass: function()
11281    {
11282        var $editor = this.editor.getElement();
11283        $editor.removeClass('over');
11284    },
11285    _loop: function(func)
11286    {
11287        var $editor = this.editor.getElement();
11288        for (var i = 0; i < this.eventsList.length; i++)
11289        {
11290            var event = this.eventsList[i] + '.redactor-events';
11291            var method = this.eventsList[i];
11292
11293            $editor[func](event, this[method].bind(this));
11294        }
11295    },
11296    _passAllToClipboard: function(e)
11297    {
11298        var clipboard = e.clipboardData;
11299        var content = this.source.getCode();
11300
11301        clipboard.setData('text/html', content);
11302        clipboard.setData('text/plain', content.toString().replace(/\n$/, ""));
11303    },
11304    _passSelectionToClipboard: function(e, data, remove)
11305    {
11306        var clipboard = e.clipboardData;
11307        var node = data.getComponent();
11308        var $node = $R.dom(node);
11309        var $cloned = $node.clone();
11310
11311        // clean
11312        $cloned.find('.redactor-component-caret').remove();
11313        $cloned.removeClass('redactor-component-active');
11314        $cloned.removeAttr('contenteditable');
11315        $cloned.removeAttr('tabindex');
11316
11317        // html
11318        var content = $cloned.get().outerHTML;
11319
11320        if (remove) this.component.remove(node);
11321
11322        clipboard.setData('text/html', content);
11323        clipboard.setData('text/plain', content.toString().replace(/\n$/, ""));
11324    },
11325    _setDragData: function(e)
11326    {
11327        e = e.originalEvent || e;
11328
11329        var dt = e.dataTransfer;
11330        dt.effectAllowed = 'move';
11331        dt.setData('text/Html', this.selection.getHtml());
11332    }
11333});
11334$R.add('module', 'container', {
11335    init: function(app)
11336    {
11337        this.app = app;
11338        this.uuid = app.uuid;
11339        this.opts = app.opts;
11340        this.lang = app.lang;
11341        this.element = app.element;
11342        this.container = app.container;
11343    },
11344    // public
11345    start: function()
11346    {
11347        this._build();
11348        this._buildAccesibility();
11349    },
11350    stop: function()
11351    {
11352        var $element = this.element.getElement();
11353        var $container = this.container.getElement();
11354
11355        $container.after($element);
11356        $container.remove();
11357        $element.show();
11358    },
11359
11360    // private
11361    _build: function()
11362    {
11363        var $element = this.element.getElement();
11364        var $container = this.container.getElement();
11365
11366        $container.addClass('redactor-box');
11367        $container.attr('dir', this.opts.direction);
11368
11369        if (this.element.isType('inline')) $container.addClass('redactor-inline');
11370
11371        $element.after($container);
11372        $container.append($element);
11373    },
11374    _buildAccesibility: function()
11375    {
11376        var $container = this.container.getElement();
11377        var $label = $R.dom('<span />');
11378
11379        $label.addClass('redactor-voice-label');
11380        $label.attr({ 'id': 'redactor-voice-' + this.uuid, 'aria-hidden': false });
11381        $label.html(this.lang.get('accessibility-help-label'));
11382
11383        $container.prepend($label);
11384    }
11385});
11386$R.add('module', 'source', {
11387    init: function(app)
11388    {
11389        this.app = app;
11390        this.uuid = app.uuid;
11391        this.opts = app.opts;
11392        this.utils = app.utils;
11393        this.element = app.element;
11394        this.source = app.source;
11395        this.editor = app.editor;
11396        this.toolbar = app.toolbar;
11397        this.cleaner = app.cleaner;
11398        this.component = app.component;
11399        this.container = app.container;
11400        this.autoparser = app.autoparser;
11401        this.selection = app.selection;
11402
11403        // local
11404        this.syncedHtml = '';
11405    },
11406    // messages
11407    onstartcode: function()
11408    {
11409        var sourceContent = this.source.getStartedContent();
11410        var $editor = this.editor.getElement();
11411        var $source = this.source.getElement();
11412
11413        // autoparse
11414        if (this.opts.autoparse && this.opts.autoparseStart)
11415        {
11416            sourceContent = this.autoparser.parse(sourceContent);
11417        }
11418
11419        // started content
11420        var startContent = this.cleaner.input(sourceContent, true, true);
11421        var syncContent = this.cleaner.output(startContent);
11422
11423        // set content
11424        $editor.html(startContent);
11425        $source.val(syncContent);
11426
11427        this.syncedHtml = syncContent;
11428        this.app.broadcast('placeholder.build');
11429        this.app.broadcast('autoparseobserve');
11430
11431        // widget's scripts
11432        this.component.executeScripts();
11433    },
11434    onstartcodeshow: function()
11435    {
11436        this.show();
11437    },
11438    ontrytosync: function()
11439    {
11440        this.sync();
11441    },
11442    onhardsync: function()
11443    {
11444        var $editor = this.editor.getElement();
11445        var html = $editor.html();
11446
11447        html = this.app.broadcast('syncBefore', html);
11448        html = this.cleaner.output(html);
11449
11450        this._syncing(html);
11451    },
11452
11453    // public
11454    start: function()
11455    {
11456        this._build();
11457        this._buildClasses();
11458    },
11459    stop: function()
11460    {
11461        var $element = this.element.getElement();
11462        var $source = this.source.getElement();
11463
11464        $element.removeClass('redactor-source redactor-source-open');
11465        $source.off('input.redactor-source');
11466        $source.removeAttr('data-gramm_editor');
11467
11468        if ($source.get().classList.length === 0) $source.removeAttr('class');
11469        if (!this.source.isNameGenerated()) $element.removeAttr('name');
11470        if (!this.element.isType('textarea')) $source.remove();
11471
11472    },
11473    getCode: function()
11474    {
11475        return this.source.getCode();
11476    },
11477
11478    // public
11479    toggle: function()
11480    {
11481        if (!this.opts.source) return;
11482
11483        var $source = this.source.getElement();
11484
11485        return ($source.hasClass('redactor-source-open')) ? this.hide() : this.show();
11486    },
11487    show: function()
11488    {
11489        if (!this.opts.source) return;
11490
11491        var $editor = this.editor.getElement();
11492        var $source = this.source.getElement();
11493        var $container = this.container.getElement();
11494
11495        var html = $source.val();
11496
11497        if (this.app.isStarted()) html = this.app.broadcast('source.open', html);
11498
11499        // get height
11500        var editorHeight = $editor.height();
11501
11502        $editor.hide();
11503        $source.height(editorHeight);
11504        $source.val(html.trim());
11505        $source.show();
11506        $source.addClass('redactor-source-open');
11507        $source.on('input.redactor-source-events', this._onChangedSource.bind(this));
11508        $source.on('keydown.redactor-source-events', this._onTabKey.bind(this));
11509        $source.on('focus.redactor-source-events', this._onFocus.bind(this));
11510
11511        if (this.opts.source.hasOwnProperty('codemirror')) {
11512            var opts = (typeof this.opts.source.codemirror === 'object') ? this.opts.source.codemirror : {};
11513            var codemirrorSrc = (typeof this.opts.source.codemirrorSrc !== 'undefined') ? this.opts.source.codemirrorSrc : CodeMirror;
11514            this.codemirror = codemirrorSrc.fromTextArea($source.get(), opts);
11515            this.codemirror.setSize(null, editorHeight);
11516            this.codemirror.on('change', function(cm, changeObj){ cm.save() });
11517            this.codemirror.on('change', this._onChangedSource.bind(this));
11518        }
11519        else {
11520            $container.addClass('redactor-source-view');
11521        }
11522
11523        // buttons
11524        setTimeout(function()
11525        {
11526            this._disableButtons();
11527            this._setActiveSourceButton();
11528
11529        }.bind(this), 100);
11530
11531        if (this.app.isStarted()) this.app.broadcast('source.opened');
11532    },
11533    hide: function()
11534    {
11535        if (!this.opts.source) return;
11536
11537        var $editor = this.editor.getElement();
11538        var $source = this.source.getElement();
11539        var $container = this.container.getElement();
11540
11541        var html = $source.val();
11542        if (this.opts.source.hasOwnProperty('codemirror')) {
11543            html = this.codemirror.getValue();
11544            this.codemirror.toTextArea();
11545        }
11546
11547
11548        // clean
11549        html = this.cleaner.input(html, true);
11550        html = (this.utils.isEmptyHtml(html)) ? this.opts.emptyHtml : html;
11551        html = this.app.broadcast('source.close', html);
11552
11553        // buttons
11554        this._enableButtons();
11555        this._setInactiveSourceButton();
11556
11557        $source.hide();
11558        $source.removeClass('redactor-source-open');
11559        $source.off('.redactor-source-events');
11560        $editor.show();
11561        $editor.html(html);
11562
11563        $container.removeClass('redactor-source-view');
11564
11565        setTimeout(function()
11566        {
11567            this.editor.startFocus();
11568
11569            // widget's scripts
11570            this.component.executeScripts();
11571
11572        }.bind(this), 0);
11573
11574        this.app.broadcast('source.closed');
11575    },
11576    sync: function()
11577    {
11578        var self = this;
11579        var $editor = this.editor.getElement();
11580        var html = $editor.html();
11581
11582        html = this.app.broadcast('syncBefore', html);
11583        html = this.cleaner.output(html);
11584
11585        if (this._isSync(html))
11586        {
11587            if (this.timeout) clearTimeout(this.timeout);
11588            this.timeout = setTimeout(function() { self._syncing(html); }, 200);
11589        }
11590    },
11591
11592    // private
11593    _build: function()
11594    {
11595        var $source = this.source.getElement();
11596        var $element = this.element.getElement();
11597
11598        $source.hide();
11599
11600        if (!this.opts.grammarly)
11601        {
11602            $source.attr('data-gramm_editor', false);
11603        }
11604
11605        if (!this.element.isType('textarea'))
11606        {
11607            $element.after($source);
11608        }
11609    },
11610    _buildClasses: function()
11611    {
11612        var $source = this.source.getElement();
11613
11614        $source.addClass('redactor-source');
11615    },
11616    _syncing: function(html)
11617    {
11618        html = this.app.broadcast('syncing', html);
11619
11620        var $source = this.source.getElement();
11621        $source.val(html);
11622
11623        this.app.broadcast('synced', html);
11624        this.app.broadcast('changed', html);
11625    },
11626    _isSync: function(html)
11627    {
11628        if (this.syncedHtml !== html)
11629        {
11630            this.syncedHtml = html;
11631            return true;
11632        }
11633
11634        return false;
11635    },
11636    _onChangedSource: function()
11637    {
11638        var $source = this.source.getElement();
11639        var html = $source.val();
11640
11641        this.app.broadcast('changed', html);
11642        this.app.broadcast('source.changed', html);
11643    },
11644    _onTabKey: function(e)
11645    {
11646        if (e.keyCode !== 9) return true;
11647
11648        e.preventDefault();
11649
11650        var $source = this.source.getElement();
11651        var el = $source.get();
11652        var start = el.selectionStart;
11653
11654        $source.val($source.val().substring(0, start) + "    " + $source.val().substring(el.selectionEnd));
11655        el.selectionStart = el.selectionEnd = start + 4;
11656    },
11657    _onFocus: function()
11658    {
11659        this.app.broadcast('sourcefocus');
11660    },
11661    _disableButtons: function()
11662    {
11663        this.toolbar.disableButtons();
11664    },
11665    _enableButtons: function()
11666    {
11667        this.toolbar.enableButtons();
11668    },
11669    _setActiveSourceButton: function()
11670    {
11671        var $btn = this.toolbar.getButton('html');
11672        $btn.enable();
11673        $btn.setActive();
11674    },
11675    _setInactiveSourceButton: function()
11676    {
11677        var $btn = this.toolbar.getButton('html');
11678        $btn.setInactive();
11679    }
11680});
11681$R.add('module', 'observer', {
11682    init: function(app)
11683    {
11684        this.app = app;
11685        this.editor = app.editor;
11686
11687        // local
11688        this.observerUnit = false;
11689    },
11690    // public
11691    start: function()
11692    {
11693        if (window.MutationObserver)
11694        {
11695            var $editor = this.editor.getElement();
11696            var el = $editor.get();
11697            this.observerUnit = this._build(el);
11698            this.observerUnit.observe(el, {
11699                 attributes: true,
11700                 subtree: true,
11701                 childList: true,
11702                 characterData: true,
11703                 characterDataOldValue: true
11704            });
11705        }
11706    },
11707    stop: function()
11708    {
11709        if (this.observerUnit) this.observerUnit.disconnect();
11710    },
11711
11712    // private
11713    _build: function(el)
11714    {
11715        var self = this;
11716        return new MutationObserver(function(mutations)
11717        {
11718            self._observe(mutations[mutations.length-1], el);
11719        });
11720    },
11721    _observe: function(mutation, el)
11722    {
11723        if (this.app.isReadOnly() || (mutation.type === 'attributes' && mutation.target === el))
11724        {
11725            return;
11726        }
11727
11728        this.app.broadcast('observe');
11729        this.app.broadcast('trytosync');
11730        this.app.broadcast('placeholder.toggle');
11731    }
11732});
11733$R.add('module', 'clicktoedit', {
11734    init: function(app)
11735    {
11736        this.app = app;
11737        this.opts = app.opts;
11738        this.source = app.source;
11739        this.editor = app.editor;
11740        this.container = app.container;
11741        this.selection = app.selection;
11742    },
11743    // messages
11744    onstartclicktoedit: function()
11745    {
11746        this.start();
11747    },
11748    onenablereadonly: function()
11749    {
11750        if (!this.opts.clickToEdit) return;
11751        if (!this._isEnabled()) this.stop();
11752    },
11753    ondisablereadonly: function()
11754    {
11755        if (!this.opts.clickToEdit) return;
11756        if (!this._isEnabled()) this.start();
11757    },
11758    onstop: function()
11759    {
11760        this.stop();
11761    },
11762
11763    // public
11764    start: function()
11765    {
11766        this._build();
11767    },
11768    stop: function()
11769    {
11770        if (this.buttonSave) this.buttonSave.stop();
11771        if (this.buttonCancel) this.buttonCancel.stop();
11772
11773        this._destroy();
11774        this.app.broadcast('disable');
11775    },
11776    enable: function()
11777    {
11778        this.app.broadcast('clickStart');
11779
11780        var isEmpty = this.editor.isEmpty();
11781        if (!isEmpty) this.selection.saveMarkers();
11782
11783        this._setFocus();
11784        this._destroy();
11785        this.app.broadcast('enable');
11786        this.buttonSave.enable();
11787        this.buttonCancel.enable();
11788
11789        if (!isEmpty) this.selection.restoreMarkers();
11790        if (isEmpty) this.editor.focus();
11791
11792        var $container = this.container.getElement();
11793        $container.addClass('redactor-clicktoedit-enabled');
11794
11795        this.source.rebuildStartedContent();
11796
11797        this.app.broadcast('startcode');
11798        this.app.broadcast('image.observe');
11799    },
11800    save: function(e)
11801    {
11802        if (e) e.preventDefault();
11803
11804        var html = this.source.getCode();
11805
11806        this.app.broadcast('disable');
11807        this.app.broadcast('clickSave', html);
11808        this.app.broadcast('clickStop');
11809        this.app.broadcast('toolbar.removeexternal');
11810        this._build();
11811    },
11812    cancel: function(e)
11813    {
11814        if (e) e.preventDefault();
11815
11816        var html = this.saved;
11817        var $editor = this.editor.getElement();
11818        $editor.html(html);
11819
11820        this.saved = '';
11821
11822        this.app.broadcast('disable');
11823        this.app.broadcast('clickCancel', html);
11824        this.app.broadcast('clickStop');
11825        this.app.broadcast('toolbar.removeexternal');
11826        this._build();
11827    },
11828
11829    // private
11830    _build: function()
11831    {
11832        // buttons
11833        this.buttonSave = $R.create('clicktoedit.button', 'save', this.app, this);
11834        this.buttonCancel = $R.create('clicktoedit.button', 'cancel', this.app, this);
11835
11836        this.buttonSave.stop();
11837        this.buttonCancel.stop();
11838
11839        var $editor = this.editor.getElement();
11840        var $container = this.container.getElement();
11841
11842        $editor.on('click.redactor-click-to-edit mouseup.redactor-click-to-edit', this.enable.bind(this));
11843        $container.addClass('redactor-over');
11844        $container.removeClass('redactor-clicktoedit-enabled');
11845    },
11846    _isEnabled: function()
11847    {
11848        return this.container.getElement().hasClass('redactor-clicktoedit-enabled');
11849    },
11850    _destroy: function()
11851    {
11852        var $editor = this.editor.getElement();
11853        var $container = this.container.getElement();
11854
11855        $editor.off('.redactor-click-to-edit');
11856        $container.removeClass('redactor-over redactor-clicktoedit-enabled');
11857    },
11858    _setFocus: function()
11859    {
11860        this.saved = this.source.getCode();
11861
11862        this.buttonSave.start();
11863        this.buttonCancel.start();
11864    }
11865});
11866$R.add('class', 'clicktoedit.button', {
11867    init: function(type, app, context)
11868    {
11869        this.app = app;
11870        this.opts = app.opts;
11871        this.toolbar = app.toolbar;
11872        this.context = context;
11873
11874        // local
11875        this.type = type;
11876        this.name = (type === 'save') ? 'clickToSave' : 'clickToCancel';
11877        this.objected = false;
11878        this.enabled = false;
11879        this.namespace = '.redactor-click-to-edit';
11880
11881        // build
11882        this._build();
11883    },
11884    enable: function()
11885    {
11886        if (!this.objected) return;
11887
11888        var data = this.opts[this.name];
11889        data.api = 'module.clicktoedit.' + this.type;
11890
11891        this.toolbar.addButton(this.type, data);
11892        this.enabled = true;
11893    },
11894    start: function()
11895    {
11896        if (this.objected) return;
11897
11898        this.$button.off(this.namespace);
11899        this.$button.show();
11900        this.$button.on('click' + this.namespace, this.context[this.type].bind(this.context));
11901    },
11902    stop: function()
11903    {
11904        if (this.objected || !this.enabled) return;
11905
11906        this.$button.hide();
11907    },
11908
11909    // private
11910    _build: function()
11911    {
11912        this.objected = (typeof this.opts[this.name] === 'object');
11913
11914        if (!this.objected)
11915        {
11916            this.$button = $R.dom(this.opts[this.name]);
11917            this.enabled = true;
11918        }
11919    }
11920});
11921$R.add('module', 'statusbar', {
11922    init: function(app)
11923    {
11924        this.app = app;
11925        this.opts = app.opts;
11926        this.element = app.element;
11927        this.statusbar = app.statusbar;
11928        this.container = app.container;
11929    },
11930    // public
11931    start: function()
11932    {
11933        if (!this.element.isType('inline'))
11934        {
11935            var $statusbar = this.statusbar.getElement();
11936            var $container = this.container.getElement();
11937
11938            $statusbar.addClass('redactor-statusbar');
11939            $container.append($statusbar);
11940        }
11941    }
11942});
11943$R.add('module', 'contextbar', {
11944    init: function(app)
11945    {
11946        this.app = app;
11947        this.opts = app.opts;
11948        this.uuid = app.uuid;
11949        this.$win = app.$win;
11950        this.$doc = app.$doc;
11951        this.$body = app.$body;
11952        this.editor = app.editor;
11953        this.toolbar = app.toolbar;
11954        this.detector = app.detector;
11955
11956        // local
11957        this.$target = (this.toolbar.isTarget()) ? this.toolbar.getTargetElement() : this.$body;
11958    },
11959    // messages
11960    onstop: function() {
11961        this.stop();
11962    },
11963    onenablereadonly: function()
11964    {
11965        this.stop();
11966    },
11967    ondisablereadonly: function()
11968    {
11969        this.start();
11970    },
11971    oncontextbar: {
11972        close: function()
11973        {
11974            this.close();
11975        }
11976    },
11977
11978    // public
11979    start: function()
11980    {
11981        if (this.opts.toolbarContext)
11982        {
11983            var $editor = this.editor.getElement();
11984
11985            this._build();
11986            $editor.on('click.redactor-context mouseup.redactor-context', this.open.bind(this));
11987
11988            if (this.opts.scrollTarget)
11989            {
11990                $R.dom(this.opts.scrollTarget).on('scroll.redactor-context', this.close.bind(this));
11991            }
11992            else if (this.opts.maxHeight !== false)
11993            {
11994                $editor.on('scroll.redactor-context', this.close.bind(this));
11995            }
11996        }
11997    },
11998    stop: function()
11999    {
12000        var $editor = this.editor.getElement();
12001        $editor.off('.redactor-context');
12002
12003        this.$doc.off('.redactor-context');
12004        this.$win.off('.redactor-context');
12005
12006        if (this.$contextbar) this.$contextbar.remove();
12007        if (this.opts.scrollTarget)
12008        {
12009            $R.dom(this.opts.scrollTarget).off('.redactor-context');
12010        }
12011    },
12012    is: function()
12013    {
12014        return (this.$contextbar && this.$contextbar.hasClass('open'));
12015    },
12016    set: function(e, node, buttons, position)
12017    {
12018        this.$contextbar.html('');
12019        this.$el = $R.dom(node);
12020
12021        // buttons
12022        for (var key in buttons)
12023        {
12024            var $btn = $R.create('contextbar.button', this.app, buttons[key]);
12025            if ($btn.html() !== '')
12026            {
12027                this.$contextbar.append($btn);
12028            }
12029        }
12030
12031        // show
12032        var pos = this._buildPosition(e, this.$el, position);
12033
12034        this.$contextbar.css(pos);
12035        this.$contextbar.show();
12036        this.$contextbar.addClass('open');
12037        this.$doc.on('click.redactor-context mouseup.redactor-context', this.close.bind(this));
12038        this.$win.on('resize.redactor-context', this.close.bind(this));
12039    },
12040    open: function(e)
12041    {
12042        setTimeout(function()
12043        {
12044            this.app.broadcast('contextbar', e, this);
12045        }.bind(this), 0);
12046    },
12047    close: function(e)
12048    {
12049        if (!this.$contextbar) return;
12050        if (e)
12051        {
12052            var $target = $R.dom(e.target);
12053            if (this.$el && $target.closest(this.$el).length !== 0)
12054            {
12055                return;
12056            }
12057        }
12058
12059        this.$contextbar.hide();
12060        this.$contextbar.removeClass('open');
12061        this.$doc.off('.redactor.context');
12062    },
12063
12064    // private
12065    _build: function()
12066    {
12067        this.$contextbar = $R.dom('<div>');
12068        this.$contextbar.attr('id', 'redactor-context-toolbar-' + this.uuid);
12069        this.$contextbar.attr('dir', this.opts.direction);
12070        this.$contextbar.addClass('redactor-context-toolbar');
12071        this.$contextbar.hide();
12072
12073        this.$target.append(this.$contextbar);
12074    },
12075    _buildPosition: function(e, $el, position)
12076    {
12077        var top, left;
12078        var isTarget = this.toolbar.isTarget();
12079        var offset = (isTarget) ? $el.position() : $el.offset();
12080
12081        var width = $el.width();
12082        var height = $el.height();
12083
12084        var barWidth = this.$contextbar.width();
12085        var barHeight = this.$contextbar.height();
12086        var docScrollTop = (isTarget) ? (this.$target.scrollTop() + this.$doc.scrollTop()) : this.$doc.scrollTop();
12087
12088        var targetOffset = this.$target.offset();
12089        var leftFix = (isTarget) ? targetOffset.left : 0;
12090        var topFix = (isTarget) ? targetOffset.top : 0;
12091
12092        if (!position)
12093        {
12094            top = (e.clientY + docScrollTop - barHeight);
12095            left = (e.clientX - barWidth/2);
12096        }
12097        else if (position === 'top')
12098        {
12099            top = (offset.top - barHeight);
12100            left = (offset.left + width/2 - barWidth/2);
12101        }
12102        else if (position === 'bottom')
12103        {
12104            top = (offset.top + height);
12105            left = (offset.left + width/2 - barWidth/2);
12106        }
12107
12108        if (left < 0) left = 0;
12109
12110        return { top: (top - topFix) + 'px', left: (left - leftFix) + 'px' };
12111    }
12112});
12113$R.add('class', 'contextbar.button', {
12114    mixins: ['dom'],
12115    init: function(app, obj)
12116    {
12117        this.app = app;
12118
12119        // local
12120        this.obj = obj;
12121
12122        // init
12123        this._init();
12124    },
12125    // private
12126    _init: function()
12127    {
12128        this.parse('<a>');
12129
12130        if (typeof this.obj.title !== 'string')
12131        {
12132            var url = this.obj.title.attr('href');
12133            this.attr('href', url);
12134
12135            if (url.search(/^#/) === -1) {
12136                this.attr('target', '_blank');
12137            }
12138
12139            this.text(this.obj.html || url);
12140        }
12141        else
12142        {
12143            this.attr('href', '#');
12144
12145            this._buildTitle();
12146            this._buildMessage();
12147        }
12148    },
12149    _buildTitle: function()
12150    {
12151        this.html(this.obj.title);
12152    },
12153    _buildMessage: function()
12154    {
12155        if (typeof this.obj.message !== 'undefined' || typeof this.obj.api !== 'undefined')
12156        {
12157            this.on('click', this._toggle.bind(this));
12158        }
12159    },
12160    _toggle: function(e)
12161    {
12162        e.preventDefault();
12163
12164        if (this.obj.message)
12165        {
12166            this.app.broadcast(this.obj.message, this.obj.args);
12167        }
12168        else if (this.obj.api)
12169        {
12170            this.app.api(this.obj.api, this.obj.args);
12171        }
12172    }
12173});
12174$R.add('module', 'toolbar', {
12175    init: function(app)
12176    {
12177        this.app = app;
12178        this.uuid = app.uuid;
12179        this.opts = app.opts;
12180        this.utils = app.utils;
12181        this.toolbar = app.toolbar;
12182        this.detector = app.detector;
12183
12184        // local
12185        this.buttons = [];
12186        this.toolbarModule = false;
12187    },
12188    // messages
12189    onsource: {
12190        open: function()
12191        {
12192            if (!this.toolbar.isAir() && this.toolbar.isFixed())
12193            {
12194                this.toolbarModule.resetPosition();
12195            }
12196        },
12197        opened: function()
12198        {
12199            if (this.toolbar.isAir() && this.toolbarModule)
12200            {
12201                this.toolbarModule.createSourceHelper();
12202            }
12203
12204            // hide tooltips
12205            setTimeout(function()
12206            {
12207                $R.dom('.re-button-tooltip-' + this.uuid).remove();
12208            }.bind(this), 100)
12209
12210        },
12211        close: function()
12212        {
12213            if (this.toolbar.isAir() && this.toolbarModule)
12214            {
12215                this.toolbarModule.destroySourceHelper();
12216            }
12217        },
12218        closed: function()
12219        {
12220            if (this.toolbar.is() && this.opts.air)
12221            {
12222                this.toolbarModule.openSelected();
12223            }
12224        }
12225    },
12226    ontoolbar: {
12227        removeexternal: function() {
12228            if (!this.opts.air && this.opts.toolbarExternal && this.opts.clickToEdit) {
12229                $R.dom(this.opts.toolbarExternal).html('');
12230            }
12231        }
12232    },
12233    onobserve: function()
12234    {
12235        if (this.toolbar.is())
12236        {
12237            this.toolbar.observe();
12238        }
12239    },
12240    onfocus: function()
12241    {
12242        this._setExternalOnFocus();
12243    },
12244    onsourcefocus: function()
12245    {
12246        this._setExternalOnFocus();
12247    },
12248    onempty: function()
12249    {
12250        if (this.toolbar.isFixed())
12251        {
12252            this.toolbarModule.resetPosition();
12253        }
12254    },
12255    onenablereadonly: function()
12256    {
12257        if (this.toolbar.isAir())
12258        {
12259            this.toolbarModule.close();
12260        }
12261    },
12262
12263    // public
12264    start: function()
12265    {
12266        if (this.toolbar.is())
12267        {
12268            this._buildButtons();
12269            this._initToolbar();
12270            this._initButtons();
12271        }
12272    },
12273    stop: function()
12274    {
12275        if (this.toolbarModule)
12276        {
12277            this.toolbarModule.stop();
12278        }
12279
12280        // stop dropdowns & tooltips
12281        $R.dom('.re-button-tooltip-' + this.uuid).remove();
12282        $R.dom('.redactor-dropdown-' + this.uuid).remove();
12283    },
12284
12285    // private
12286    _buildButtons: function()
12287    {
12288        this.buttons = this.opts.buttons.concat();
12289
12290        this._buildImageButton();
12291        this._buildFileButton();
12292        this._buildSourceButton();
12293        this._buildAdditionalButtons();
12294        this._buildHiddenButtons();
12295    },
12296    _buildImageButton: function()
12297    {
12298        if (!this.opts.imageUpload && !this.opts.imageManagerJson) this.utils.removeFromArrayByValue(this.buttons, 'image');
12299    },
12300    _buildFileButton: function()
12301    {
12302        if (!this.opts.fileUpload) this.utils.removeFromArrayByValue(this.buttons, 'file');
12303    },
12304    _buildSourceButton: function()
12305    {
12306        if (!this.opts.source) this.utils.removeFromArrayByValue(this.buttons, 'html');
12307    },
12308    _buildAdditionalButtons: function()
12309    {
12310        // end
12311        if (this.opts.buttonsAdd.length !== 0)
12312        {
12313            this.opts.buttonsAdd = this._removeExistButtons(this.opts.buttonsAdd);
12314            this.buttons = this.buttons.concat(this.opts.buttonsAdd);
12315        }
12316
12317        // beginning
12318        if (this.opts.buttonsAddFirst.length !== 0)
12319        {
12320            this.opts.buttonsAddFirst = this._removeExistButtons(this.opts.buttonsAddFirst);
12321            this.buttons.unshift(this.opts.buttonsAddFirst);
12322        }
12323
12324        var index, btns;
12325
12326        // after
12327        if (this.opts.buttonsAddAfter !== false)
12328        {
12329            index = this.buttons.indexOf(this.opts.buttonsAddAfter.after) + 1;
12330            btns = this.opts.buttonsAddAfter.buttons;
12331            for (var i = 0; i < btns.length; i++)
12332            {
12333                this.buttons.splice(index+i, 0, btns[i]);
12334            }
12335        }
12336
12337        // before
12338        if (this.opts.buttonsAddBefore !== false)
12339        {
12340            index = this.buttons.indexOf(this.opts.buttonsAddBefore.before)+1;
12341            btns = this.opts.buttonsAddBefore.buttons;
12342            for (var i = 0; i < btns.length; i++)
12343            {
12344                this.buttons.splice(index-(1-i), 0, btns[i]);
12345            }
12346        }
12347    },
12348    _buildHiddenButtons: function()
12349    {
12350        if (this.opts.buttonsHide.length !== 0) {
12351            var buttons = this.opts.buttonsHide;
12352            for (var i = 0; i < buttons.length; i++) {
12353                this.utils.removeFromArrayByValue(this.buttons, buttons[i]);
12354            }
12355        }
12356
12357        if (this.detector.isMobile() && this.opts.buttonsHideOnMobile.length !== 0) {
12358            var buttons = this.opts.buttonsHideOnMobile;
12359            for (var i = 0; i < buttons.length; i++) {
12360                this.utils.removeFromArrayByValue(this.buttons, buttons[i]);
12361            }
12362        }
12363    },
12364    _removeExistButtons: function(buttons)
12365    {
12366        for (var i = 0; i < buttons.length; i++)
12367        {
12368            if (this.opts.buttons.indexOf(buttons[i]) !== -1)
12369            {
12370                this.utils.removeFromArrayByValue(buttons, buttons[i]);
12371            }
12372        }
12373
12374        return buttons;
12375    },
12376    _setExternalOnFocus: function()
12377    {
12378        if (!this.opts.air && this.opts.toolbarExternal)
12379        {
12380            this.toolbarModule.setExternal();
12381        }
12382    },
12383    _initToolbar: function()
12384    {
12385        this.toolbarModule = (this.opts.air) ? $R.create('toolbar.air', this.app) : $R.create('toolbar.standard', this.app);
12386    },
12387    _initButtons: function()
12388    {
12389        this.toolbar.setButtons(this.buttons);
12390
12391        for (var i = 0; i < this.buttons.length; i++)
12392        {
12393            var name = this.buttons[i];
12394            if ($R.buttons[name])
12395            {
12396                this.toolbar.addButton(name, $R.extend(true, {}, $R.buttons[name]), false, false, true);
12397            }
12398        }
12399    }
12400});
12401$R.add('class', 'toolbar.air', {
12402    init: function(app)
12403    {
12404        this.app = app;
12405        this.uuid = app.uuid;
12406        this.$doc = app.$doc;
12407        this.$win = app.$win;
12408        this.utils = app.utils;
12409        this.editor = app.editor;
12410        this.animate = app.animate;
12411        this.toolbar = app.toolbar;
12412        this.container = app.container;
12413        this.inspector = app.inspector;
12414        this.selection = app.selection;
12415
12416        // local
12417        this.clicks = 0;
12418
12419        // init
12420        this._init();
12421    },
12422    // public
12423    stop: function()
12424    {
12425        var $wrapper = this.toolbar.getWrapper();
12426        $wrapper.remove();
12427
12428        var $editor = this.editor.getElement();
12429        $editor.off('.redactor-air-trigger-' + this.uuid);
12430
12431        this.$doc.off('.redactor-air-' + this.uuid);
12432        this.$doc.off('.redactor-air-trigger-' + this.uuid);
12433
12434        this.toolbar.stopObservers();
12435    },
12436    createSourceHelper: function()
12437    {
12438        this.$airHelper = $R.dom('<span>');
12439        this.$airHelper.addClass('redactor-air-helper');
12440        this.$airHelper.html('<i class="re-icon-html"></i>');
12441        this.$airHelper.on('click', function(e)
12442        {
12443            e.preventDefault();
12444            this.app.api('module.source.hide');
12445
12446        }.bind(this));
12447
12448        var $container = this.container.getElement();
12449        $container.append(this.$airHelper);
12450    },
12451    destroySourceHelper: function()
12452    {
12453        if (this.$airHelper) this.$airHelper.remove();
12454    },
12455    openSelected: function()
12456    {
12457        setTimeout(function()
12458        {
12459            if (this._isSelection()) this._open(false);
12460
12461        }.bind(this), 0);
12462    },
12463    close: function()
12464    {
12465        this.$doc.off('.redactor-air-' + this.uuid);
12466
12467        var $toolbar = this.toolbar.getElement();
12468        $toolbar.removeClass('open');
12469        $toolbar.hide();
12470    },
12471
12472    // private
12473    _init: function()
12474    {
12475        this.toolbar.create();
12476
12477        var $wrapper = this.toolbar.getWrapper();
12478        var $toolbar = this.toolbar.getElement();
12479        var $editor = this.editor.getElement();
12480        var $container = this.container.getElement();
12481
12482        $wrapper.addClass('redactor-toolbar-wrapper-air');
12483        $toolbar.addClass('redactor-air');
12484        //$toolbar.addClass('redactor-animate-hide');
12485        $toolbar.hide();
12486
12487        $wrapper.append($toolbar);
12488        $container.prepend($wrapper);
12489
12490        // open selected
12491        this.openSelected();
12492
12493        // events
12494        this.$doc.on('mouseup.redactor-air-trigger-' + this.uuid, this._open.bind(this));
12495        $editor.on('keyup.redactor-air-trigger-' + this.uuid, this._openCmd.bind(this));
12496    },
12497    _isSelection: function()
12498    {
12499        return (this.selection.is() && !this.selection.isCollapsed());
12500    },
12501    _isOpened: function()
12502    {
12503        var $toolbar = this.toolbar.getElement();
12504
12505        return $toolbar.hasClass('open');
12506    },
12507    _open: function(e)
12508    {
12509        var target = (e) ? e.target : false;
12510        var $el = (e) ? $R.dom(e.target) : false;
12511        var dataTarget = this.inspector.parse(target);
12512        var isComponent = (dataTarget.isComponent() && !dataTarget.isComponentType('table'));
12513        var isFigcaption = (dataTarget.isFigcaption());
12514        var isModalTarget = ($el && $el.closest('.redactor-modal').length !== 0);
12515        var isButtonCall = (e && $el.closest('.re-button').length !== 0);
12516        var isDropdownCall = (e && $el.closest('.redactor-dropdown').length !== 0);
12517
12518        if (isDropdownCall || isButtonCall || isModalTarget || isFigcaption || isComponent || this.toolbar.isContextBar() || !this._isSelection())
12519        {
12520            return;
12521        }
12522
12523        var pos = this.selection.getPosition();
12524
12525        setTimeout(function()
12526        {
12527            if (this.app.isReadOnly()) return;
12528            if (this._isSelection()) this._doOpen(pos);
12529
12530        }.bind(this), 1);
12531
12532    },
12533    _openCmd: function()
12534    {
12535        if (this.selection.isAll())
12536        {
12537            var $toolbar = this.toolbar.getElement();
12538            var pos = this.selection.getPosition();
12539
12540            pos.top = (pos.top < 20) ? 0 : pos.top - $toolbar.height();
12541            pos.height = 0;
12542
12543            this._doOpen(pos);
12544        }
12545    },
12546    _doOpen: function(pos)
12547    {
12548        var $wrapper = this.toolbar.getWrapper();
12549        var $toolbar = this.toolbar.getElement();
12550        var $container = this.container.getElement();
12551        var containerOffset = $container.offset();
12552        var leftFix = 0;
12553        var winWidth = this.$win.width();
12554        var toolbarWidth = $toolbar.width();
12555
12556        if (winWidth < (pos.left + toolbarWidth))
12557        {
12558            var selPos = this.selection.getPosition();
12559            leftFix = toolbarWidth - selPos.width;
12560        }
12561
12562        $wrapper.css({
12563            left: (pos.left - containerOffset.left - leftFix) + 'px',
12564            top: (pos.top - containerOffset.top + pos.height + this.$doc.scrollTop()) + 'px'
12565        });
12566
12567        this.app.broadcast('airOpen');
12568        $toolbar.addClass('open');
12569        $toolbar.show();
12570
12571        this.$doc.on('click.redactor-air-' + this.uuid, this._close.bind(this));
12572        this.$doc.on('keydown.redactor-air-' + this.uuid, this._close.bind(this));
12573        this.app.broadcast('airOpened');
12574    },
12575    _close: function(e)
12576    {
12577        var $el = (e) ? $R.dom(e.target) : false;
12578        var isDropdownCall = (e && $el.closest('[data-dropdown], .redactor-dropdown-not-close').length !== 0);
12579        var isButtonCall = (!isDropdownCall && e && $el.closest('.re-button').length !== 0);
12580
12581        if (!isButtonCall && (isDropdownCall || !this._isOpened()))
12582        {
12583            return;
12584        }
12585
12586        // close
12587        this.app.broadcast('airClose');
12588
12589        this.close();
12590        this.app.broadcast('airClosed');
12591    }
12592});
12593$R.add('class', 'toolbar.fixed', {
12594    init: function(app)
12595    {
12596        this.app = app;
12597        this.uuid = app.uuid;
12598        this.opts = app.opts;
12599        this.$doc = app.$doc;
12600        this.$win = app.$win;
12601        this.editor = app.editor;
12602        this.toolbar = app.toolbar;
12603        this.detector = app.detector;
12604        this.container = app.container;
12605
12606        // init
12607        this._init();
12608    },
12609    // public
12610    stop: function()
12611    {
12612        this.$fixedTarget.off('.redactor-toolbar-' + this.uuid);
12613        this.$win.off('.redactor-toolbar-' + this.uuid);
12614    },
12615    reset: function()
12616    {
12617        var $toolbar = this.toolbar.getElement();
12618        var $wrapper = this.toolbar.getWrapper();
12619
12620        $wrapper.css('height', '');
12621        $toolbar.removeClass('redactor-toolbar-fixed');
12622        $toolbar.css({ position: '', top: '', left: '', width: '' });
12623
12624        var dropdown = this.toolbar.getDropdown();
12625        if (dropdown) dropdown.updatePosition();
12626    },
12627
12628    // private
12629    _init: function()
12630    {
12631        this.$fixedTarget = (this.toolbar.isTarget()) ? this.toolbar.getTargetElement() : this.$win;
12632        this._doFixed();
12633
12634        if (this.toolbar.isTarget())
12635        {
12636            this.$win.on('scroll.redactor-toolbar-' + this.uuid, this._doFixed.bind(this));
12637            this.$win.on('resize.redactor-toolbar-' + this.uuid, this._doFixed.bind(this));
12638        }
12639
12640        this.$fixedTarget.on('scroll.redactor-toolbar-' + this.uuid, this._doFixed.bind(this));
12641        this.$fixedTarget.on('resize.redactor-toolbar-' + this.uuid, this._doFixed.bind(this));
12642    },
12643    _doFixed: function()
12644    {
12645        var $editor = this.editor.getElement();
12646        var $container = this.container.getElement();
12647        var $toolbar = this.toolbar.getElement();
12648        var $wrapper = this.toolbar.getWrapper();
12649
12650        if (this.editor.isSourceMode())
12651        {
12652            return;
12653        }
12654
12655        var $targets = $container.parents().filter(function(node)
12656        {
12657            return (getComputedStyle(node, null).display === 'none') ? node : false;
12658        });
12659
12660        // has hidden parent
12661        if ($targets.length !== 0) return;
12662
12663        var isHeight = ($editor.height() < 100);
12664        var isEmpty = this.editor.isEmpty();
12665
12666        if (isHeight || isEmpty) {
12667            this.reset();
12668            return;
12669        }
12670
12671        if (this.editor.isSourceMode()) {
12672            return;
12673        }
12674
12675        var toolbarHeight = $toolbar.height();
12676        var toleranceEnd = 60;
12677        var containerOffset = (this.toolbar.isTarget()) ? $container.position() : $container.offset();
12678        var boxOffset = containerOffset.top;
12679        var boxEnd = boxOffset + $container.height() - toleranceEnd;
12680        var scrollOffset = this.$fixedTarget.scrollTop() + this.opts.toolbarFixedTopOffset;
12681        var top = (!this.toolbar.isTarget()) ? 0 : this.$fixedTarget.offset().top - this.$win.scrollTop();
12682        if (this.toolbar.isTarget() && this.$fixedTarget.css('position') === 'fixed') {
12683            var bs = (this.$fixedTarget.hasClass('modal') && this.$fixedTarget.hasClass('fade'));
12684            var bsFix = (bs) ? $container.closest('.modal-dialog').position().top : 0;
12685            top = this.$fixedTarget.scrollTop() - bsFix;
12686        }
12687
12688
12689        if (scrollOffset > boxOffset && scrollOffset < boxEnd)
12690        {
12691            var position = (this.detector.isDesktop()) ? 'fixed' : 'absolute';
12692            top = (this.detector.isDesktop()) ? top : (scrollOffset - boxOffset);
12693
12694            if (this.detector.isMobile())
12695            {
12696                if (this.fixedScrollTimeout)
12697                {
12698                    clearTimeout(this.fixedScrollTimeout);
12699                }
12700
12701                $toolbar.hide();
12702                this.fixedScrollTimeout = setTimeout(function()
12703                {
12704                    $toolbar.show();
12705                }, 250);
12706            }
12707
12708            $wrapper.height(toolbarHeight);
12709            $toolbar.addClass('redactor-toolbar-fixed');
12710
12711            if ($container.hasClass('redactor-box-fullscreen'))
12712            {
12713                $toolbar.css({
12714                    position: position,
12715                    top: '0px',
12716                    width: $container.width() + 'px'
12717                });
12718            }
12719            else
12720            {
12721                $toolbar.css({
12722                    position: position,
12723                    top: (top + this.opts.toolbarFixedTopOffset) + 'px',
12724                    width: $container.width() + 'px'
12725                });
12726            }
12727
12728            var dropdown = this.toolbar.getDropdown();
12729            if (dropdown) dropdown.updatePosition();
12730
12731            this.app.broadcast('toolbar.fixed');
12732        }
12733        else
12734        {
12735            this.reset();
12736            this.app.broadcast('toolbar.unfixed');
12737        }
12738    }
12739});
12740$R.add('class', 'toolbar.standard', {
12741    init: function(app)
12742    {
12743        this.app = app;
12744        this.opts = app.opts;
12745        this.uuid = app.uuid;
12746        this.$body = app.$body;
12747        this.toolbar = app.toolbar;
12748        this.container = app.container;
12749
12750        // local
12751        this.isExternalMultiple = false;
12752        this.toolbarFixed = false;
12753
12754        // init
12755        this._init();
12756    },
12757    // public
12758    stop: function()
12759    {
12760        var $wrapper = this.toolbar.getWrapper();
12761        $wrapper.remove();
12762
12763        if (this.toolbarFixed) this.toolbarFixed.stop();
12764        if (this.opts.toolbarExternal) this._findToolbars();
12765
12766        this.toolbar.stopObservers();
12767        this.$body.find('.re-button-tooltip-' + this.uuid).remove();
12768    },
12769    setExternal: function()
12770    {
12771        this._findToolbars();
12772        if (this.isExternalMultiple)
12773        {
12774            this.$toolbars.hide();
12775            var $current = this.$external.find('.redactor-toolbar-external-' + this.uuid);
12776            $current.show();
12777        }
12778    },
12779    resetPosition: function()
12780    {
12781        if (this.toolbarFixed) this.toolbarFixed.reset();
12782    },
12783
12784    // private
12785    _init: function()
12786    {
12787        this._build();
12788
12789        if (this.opts.toolbarExternal)
12790        {
12791            this._buildExternal();
12792        }
12793        else
12794        {
12795            this._buildFixed();
12796
12797            var $toolbar = this.toolbar.getElement();
12798            $toolbar.show();
12799        }
12800    },
12801    _build: function()
12802    {
12803        this.toolbar.create();
12804
12805        var $wrapper = this.toolbar.getWrapper();
12806        var $toolbar = this.toolbar.getElement();
12807
12808        $wrapper.addClass('redactor-toolbar-wrapper');
12809        $toolbar.addClass('redactor-toolbar');
12810        $toolbar.hide();
12811        $wrapper.append($toolbar);
12812
12813        if (!this.opts.toolbarExternal)
12814        {
12815            var $container = this.container.getElement();
12816            $container.prepend($wrapper);
12817        }
12818    },
12819    _buildExternal: function()
12820    {
12821        this._initExternal();
12822        this._findToolbars();
12823
12824        if (this.isExternalMultiple)
12825        {
12826            this._hideToolbarsExceptFirst();
12827        }
12828        else
12829        {
12830            var $toolbar = this.toolbar.getElement();
12831            $toolbar.show();
12832        }
12833    },
12834    _buildFixed: function()
12835    {
12836        if (this.opts.toolbarFixed)
12837        {
12838            this.toolbarFixed = $R.create('toolbar.fixed', this.app);
12839        }
12840    },
12841    _initExternal: function()
12842    {
12843        var $toolbar = this.toolbar.getElement();
12844        var $wrapper = this.toolbar.getElement();
12845
12846        $toolbar.addClass('redactor-toolbar-external redactor-toolbar-external-' + this.uuid);
12847
12848        this.$external = $R.dom(this.opts.toolbarExternal);
12849        this.$external.append($wrapper);
12850
12851    },
12852    _findToolbars: function()
12853    {
12854        this.$toolbars = this.$external.find('.redactor-toolbar-external');
12855        this.isExternalMultiple = (this.$toolbars.length > 1);
12856    },
12857    _hideToolbarsExceptFirst: function()
12858    {
12859        this.$toolbars.hide();
12860        var $first = this.$toolbars.first();
12861        $first.show();
12862    }
12863});
12864$R.add('module', 'line', {
12865    init: function(app)
12866    {
12867        this.app = app;
12868        this.lang = app.lang;
12869        this.component = app.component;
12870        this.inspector = app.inspector;
12871        this.insertion = app.insertion;
12872    },
12873    // messages
12874    oncontextbar: function(e, contextbar)
12875    {
12876        var data = this.inspector.parse(e.target);
12877        if (data.isComponentType('line'))
12878        {
12879            var node = data.getComponent();
12880            var buttons = {
12881                "remove": {
12882                    title: this.lang.get('delete'),
12883                    api: 'module.line.remove',
12884                    args: node
12885                }
12886            };
12887
12888            contextbar.set(e, node, buttons, 'bottom');
12889        }
12890
12891    },
12892
12893    // public
12894    insert: function()
12895    {
12896        var line = this.component.create('line');
12897        this.insertion.insertRaw(line);
12898    },
12899    remove: function(node)
12900    {
12901        this.component.remove(node);
12902    }
12903});
12904$R.add('class', 'line.component', {
12905    mixins: ['dom', 'component'],
12906    init: function(app, el)
12907    {
12908        this.app = app;
12909
12910        // init
12911        return (el && el.cmnt !== undefined) ? el : this._init(el);
12912    },
12913    // private
12914    _init: function(el)
12915    {
12916        var wrapper, element;
12917        if (typeof el !== 'undefined')
12918        {
12919            var $node = $R.dom(el);
12920            var node = $node.get();
12921
12922            if (node.tagName === 'HR') element = node;
12923            else if (node.tagName === 'FIGURE')
12924            {
12925                wrapper = node;
12926                element = $node.find('hr').get();
12927            }
12928        }
12929
12930        this._buildWrapper(wrapper);
12931        this._buildElement(element);
12932        this._initWrapper();
12933    },
12934    _buildElement: function(node)
12935    {
12936        if (node)
12937        {
12938            this.$element = $R.dom(node);
12939        }
12940        else
12941        {
12942            this.$element = $R.dom('<hr>');
12943            this.append(this.$element);
12944        }
12945    },
12946    _buildWrapper: function(node)
12947    {
12948        node = node || '<figure>';
12949
12950        this.parse(node);
12951    },
12952    _initWrapper: function()
12953    {
12954        this.addClass('redactor-component');
12955        this.attr({
12956            'data-redactor-type': 'line',
12957            'tabindex': '-1',
12958            'contenteditable': false
12959        });
12960    }
12961});
12962$R.add('module', 'link', {
12963    modals: {
12964        'link':
12965            '<form action=""> \
12966                <div class="form-item"> \
12967                    <label for="modal-link-url">URL <span class="req">*</span></label> \
12968                    <input type="text" id="modal-link-url" name="url"> \
12969                </div> \
12970                <div class="form-item"> \
12971                    <label for="modal-link-text">## text ##</label> \
12972                    <input type="text" id="modal-link-text" name="text"> \
12973                </div> \
12974                <div class="form-item form-item-title"> \
12975                    <label for="modal-link-title">## title ##</label> \
12976                    <input type="text" id="modal-link-title" name="title"> \
12977                </div> \
12978                <div class="form-item form-item-target"> \
12979                    <label class="checkbox"> \
12980                        <input type="checkbox" name="target"> ## link-in-new-tab ## \
12981                    </label> \
12982                </div> \
12983            </form>'
12984    },
12985    init: function(app)
12986    {
12987        this.app = app;
12988        this.opts = app.opts;
12989        this.lang = app.lang;
12990        this.caret = app.caret;
12991        this.utils = app.utils;
12992        this.inline = app.inline;
12993        this.editor = app.editor;
12994        this.inspector = app.inspector;
12995        this.insertion = app.insertion;
12996        this.selection = app.selection;
12997
12998        // local
12999        this.isCurrentLink = false;
13000        this.currentText = false;
13001    },
13002    // messages
13003    onmodal: {
13004        link: {
13005            open: function($modal, $form)
13006            {
13007                this._setFormData($form, $modal);
13008            },
13009            opened: function($modal, $form)
13010            {
13011                this._setFormFocus($form);
13012            },
13013            update: function($modal, $form)
13014            {
13015                var data = $form.getData();
13016                if (this._validateData($form, data))
13017                {
13018                    this._update(data);
13019                }
13020            },
13021            insert: function($modal, $form)
13022            {
13023                var data = $form.getData();
13024                if (this._validateData($form, data))
13025                {
13026                    this._insert(data);
13027                }
13028            },
13029            unlink: function()
13030            {
13031                this._unlink();
13032            }
13033        }
13034    },
13035    onbutton: {
13036        link: {
13037            observe: function(button)
13038            {
13039                this._observeButton(button);
13040            }
13041        }
13042    },
13043    ondropdown: {
13044        link: {
13045            observe: function(dropdown)
13046            {
13047                this._observeUnlink(dropdown);
13048                this._observeEdit(dropdown);
13049            }
13050        }
13051    },
13052    oncontextbar: function(e, contextbar)
13053    {
13054        var current = this._getCurrent();
13055        var data = this.inspector.parse(current);
13056        if (data.isLink() || data.isFile())
13057        {
13058            var node = (data.isFile()) ? data.getFile() : data.getLink();
13059            var $el = $R.dom(node);
13060
13061            var $point = $R.dom('<a>');
13062            var url = $el.attr('href');
13063
13064            $point.text(this._truncateText(url));
13065            $point.attr('href', url);
13066            $point.attr('target', '_blank');
13067
13068            var buttons = {
13069                "link": {
13070                    title: $point,
13071                    html: this._truncateText(url)
13072                },
13073                "edit": {
13074                    title: this.lang.get('edit'),
13075                    api: 'module.link.open'
13076                },
13077                "unlink": {
13078                    title: this.lang.get('unlink'),
13079                    api: 'module.link.unlink'
13080                }
13081            };
13082
13083            contextbar.set(e, node, buttons, 'bottom');
13084        }
13085    },
13086
13087    // public
13088    open: function()
13089    {
13090        this.$link = this._buildCurrent();
13091        this.app.api('module.modal.build', this._getModalData());
13092    },
13093    insert: function(data)
13094    {
13095        this._insert(data);
13096    },
13097    update: function(data)
13098    {
13099        this._update(data);
13100    },
13101    unlink: function()
13102    {
13103        this._unlink();
13104    },
13105
13106    // private
13107    _observeButton: function(button)
13108    {
13109        var current = this.selection.getCurrent();
13110        var data = this.inspector.parse(current);
13111
13112        if (data.isPre() || data.isCode())
13113        {
13114            button.disable();
13115        }
13116        else
13117        {
13118            button.enable();
13119        }
13120    },
13121    _observeUnlink: function(dropdown)
13122    {
13123        var $item = dropdown.getItem('unlink');
13124        var links = this._getLinks();
13125
13126        if (links.length === 0) $item.disable();
13127        else                    $item.enable();
13128    },
13129    _observeEdit: function(dropdown)
13130    {
13131        var current = this._getCurrent();
13132        var $item = dropdown.getItem('link');
13133
13134        var data = this.inspector.parse(current);
13135        var title = (data.isLink() || data.isFile()) ? this.lang.get('link-edit') : this.lang.get('link-insert');
13136
13137        $item.setTitle(title);
13138    },
13139    _unlink: function()
13140    {
13141        this.app.api('module.modal.close');
13142
13143        var elms = [];
13144        var nodes = this._getLinks();
13145
13146        this.selection.save();
13147        for (var i = 0; i < nodes.length; i++)
13148        {
13149            var $link = $R.create('link.component', this.app, nodes[i]);
13150            elms.push(this.selection.getElement(nodes[i]));
13151
13152            $link.unwrap();
13153
13154            // callback
13155            this.app.broadcast('link.deleted', $link);
13156        }
13157        this.selection.restore();
13158
13159        // normalize
13160        for (var i = 0; i < elms.length; i++)
13161        {
13162            var el = (elms[i]) ? elms[i] : this.editor.getElement();
13163            this.utils.normalizeTextNodes(el);
13164        }
13165
13166        this._resetCurrent();
13167    },
13168    _update: function(data)
13169    {
13170        this.app.api('module.modal.close');
13171
13172        var nodes = this._getLinks();
13173        this._setLinkData(nodes, data, 'updated');
13174        this._resetCurrent();
13175
13176        // callback
13177        this.app.broadcast('link.changed', nodes);
13178    },
13179    _insert: function(data)
13180    {
13181        this.app.api('module.modal.close');
13182
13183        var links = this._getLinks();
13184
13185        if (!this._insertSingle(links, data))
13186        {
13187            this._removeInSelection(links);
13188            this._insertMultiple(data);
13189        }
13190
13191        this._resetCurrent();
13192    },
13193    _removeInSelection: function(links)
13194    {
13195        this.selection.save();
13196        for (var i = 0; i < links.length; i++)
13197        {
13198            var $link = $R.create('link.component', this.app, links[i]);
13199            var $clonedLink = $link.clone();
13200            $link.unwrap();
13201
13202            // callback
13203            this.app.broadcast('link.deleted', $clonedLink);
13204        }
13205        this.selection.restore();
13206    },
13207    _insertMultiple: function(data)
13208    {
13209        var range = this.selection.getRange();
13210        if (range && this._isCurrentTextChanged(data))
13211        {
13212            this._deleteContents(range);
13213        }
13214
13215        var nodes = this.inline.format({ tag: 'a' });
13216
13217        this._setLinkData(nodes, data, 'inserted');
13218    },
13219    _insertSingle: function(links, data)
13220    {
13221        var inline = this.selection.getInline();
13222        if (links.length === 1 && ((links[0].textContext === this.selection.getText()) || (inline && inline.tagName === 'A')))
13223        {
13224            var $link = $R.create('link.component', this.app, links[0]);
13225
13226            $link.setData(data);
13227            this.caret.setAfter($link);
13228
13229            // callback
13230            this.app.broadcast('link.inserted', $link);
13231
13232            return true;
13233        }
13234
13235        return false;
13236    },
13237    _setLinkData: function(nodes, data, type)
13238    {
13239        data.text = (data.text.trim() === '') ? this._truncateText(data.url) : data.text;
13240
13241        var isTextChanged = (!this.currentText || this.currentText !== data.text);
13242
13243        this.selection.save();
13244        for (var i = 0; i < nodes.length; i++)
13245        {
13246            var $link = $R.create('link.component', this.app, nodes[i]);
13247            var linkData = {};
13248
13249            if (data.text && isTextChanged) linkData.text = data.text;
13250            if (data.url) linkData.url = data.url;
13251            if (data.title !== undefined) linkData.title = data.title;
13252            if (data.target !== undefined) linkData.target = data.target;
13253
13254            $link.setData(linkData);
13255
13256            // callback
13257            this.app.broadcast('link.' + type, $link);
13258        }
13259
13260        setTimeout(this.selection.restore.bind(this.selection), 0);
13261    },
13262    _deleteContents: function(range)
13263    {
13264        var html = this.selection.getHtml();
13265        var parsed = this.utils.parseHtml(html);
13266        var first = parsed.nodes[0];
13267
13268        if (first && first.nodeType !== 3)
13269        {
13270            var tag = first.tagName.toLowerCase();
13271            var container = document.createElement(tag);
13272
13273            this.insertion.insertNode(container, 'start');
13274        }
13275        else
13276        {
13277            range.deleteContents();
13278        }
13279    },
13280    _getModalData: function()
13281    {
13282        var commands;
13283        if (this._isLink())
13284        {
13285           commands = {
13286                update: { title: this.lang.get('save') },
13287                unlink: { title: this.lang.get('unlink'), type: 'danger' },
13288                cancel: { title: this.lang.get('cancel') }
13289            };
13290        }
13291        else
13292        {
13293            commands = {
13294                insert: { title: this.lang.get('insert') },
13295                cancel: { title: this.lang.get('cancel') }
13296            };
13297        }
13298
13299        var modalData = {
13300            name: 'link',
13301            title: (this._isLink()) ? this.lang.get('link-edit') : this.lang.get('link-insert'),
13302            handle: (this._isLink()) ? 'update' : 'insert',
13303            commands: commands
13304        };
13305
13306
13307        return modalData;
13308    },
13309    _isLink: function()
13310    {
13311        return this.currentLink;
13312    },
13313    _isCurrentTextChanged: function(data)
13314    {
13315        return (this.currentText && this.currentText !== data.text);
13316    },
13317    _buildCurrent: function()
13318    {
13319        var current = this._getCurrent();
13320        var data = this.inspector.parse(current);
13321        var $link;
13322
13323        if (data.isLink() || data.isFile())
13324        {
13325            this.currentLink = true;
13326
13327            $link = (data.isFile()) ? data.getFile() : data.getLink();
13328            $link = $R.create('link.component', this.app, $link);
13329        }
13330        else
13331        {
13332            this.currentLink = false;
13333
13334            $link = $R.create('link.component', this.app);
13335            var linkData = {
13336                text: this.selection.getText()
13337            };
13338
13339            $link.setData(linkData);
13340        }
13341
13342        return $link;
13343    },
13344    _getCurrent: function()
13345    {
13346        return this.selection.getInlinesAllSelected({ tags: ['a'] })[0];
13347    },
13348    _getLinks: function()
13349    {
13350        var links = this.selection.getInlines({ all: true, tags: ['a'] });
13351        var arr = [];
13352        for (var i = 0; i < links.length; i++)
13353        {
13354            var data = this.inspector.parse(links[i]);
13355            if (data.isLink() || data.isFile())
13356            {
13357                arr.push(links[i]);
13358            }
13359        }
13360
13361        return arr;
13362    },
13363    _resetCurrent: function()
13364    {
13365        this.isCurrentLink = false;
13366        this.currentText = false;
13367    },
13368    _truncateText: function(url)
13369    {
13370        return (url && url.length > this.opts.linkSize) ? url.substring(0, this.opts.linkSize) + '...' : url;
13371    },
13372    _validateData: function($form, data)
13373    {
13374        return (data.url.trim() === '') ? $form.setError('url') : true;
13375    },
13376    _setFormFocus: function($form)
13377    {
13378        $form.getField('url').focus();
13379    },
13380    _setFormData: function($form, $modal)
13381    {
13382        var linkData = this.$link.getData();
13383        var data = {
13384            url: linkData.url,
13385            text: linkData.text,
13386            title: linkData.title,
13387            target: (this.opts.linkTarget || linkData.target)
13388        };
13389
13390        if (!this.opts.linkNewTab) $modal.find('.form-item-target').hide();
13391        if (!this.opts.linkTitle) $modal.find('.form-item-title').hide();
13392
13393        $form.setData(data);
13394        this.currentText = $form.getField('text').val();
13395    }
13396});
13397$R.add('class', 'link.component', {
13398    mixins: ['dom', 'component'],
13399    init: function(app, el)
13400    {
13401        this.app = app;
13402        this.opts = app.opts;
13403
13404        // local
13405        this.reUrl = /^(?:(?:(?:https?|ftp):)?\/\/)?(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:[/?#]\S*)?$/i;
13406
13407        // init
13408        return (el && el.cmnt !== undefined) ? el :  this._init(el);
13409    },
13410
13411    // public
13412    setData: function(data)
13413    {
13414        for (var name in data)
13415        {
13416            this._set(name, data[name]);
13417        }
13418    },
13419    getData: function()
13420    {
13421        var names = ['url', 'text', 'target', 'title'];
13422        var data = {};
13423
13424        for (var i = 0; i < names.length; i++)
13425        {
13426            data[names[i]] = this._get(names[i]);
13427        }
13428
13429        return data;
13430    },
13431
13432    // private
13433    _init: function(el)
13434    {
13435        var $el = $R.dom(el);
13436        if (el === undefined)
13437        {
13438            this.parse('<a>');
13439        }
13440        else
13441        {
13442            this.parse($el);
13443        }
13444    },
13445    _set: function(name, value)
13446    {
13447        this['_set_' + name](value);
13448    },
13449    _get: function(name)
13450    {
13451        return this['_get_' + name]();
13452    },
13453    _get_target: function()
13454    {
13455        return (this.attr('target')) ? this.attr('target') : false;
13456    },
13457    _get_url: function()
13458    {
13459        return this.attr('href');
13460    },
13461    _get_title: function()
13462    {
13463        return this.attr('title');
13464    },
13465    _get_text: function()
13466    {
13467        return this._getContext().text();
13468    },
13469    _getContext: function()
13470    {
13471        return this._findDeepestChild(this).element;
13472    },
13473    _set_target: function(target)
13474    {
13475        if (target === false) this.removeAttr('target');
13476        else if (target)
13477        {
13478            this.attr('target', (target === true) ? '_blank' : target);
13479        }
13480    },
13481    _set_text: function(text)
13482    {
13483        text = this._escapeHtml(text);
13484        this._getContext().html(text);
13485    },
13486    _set_title: function(title)
13487    {
13488        if (!title || title === '') this.removeAttr('title');
13489        else this.attr('title', title);
13490    },
13491    _set_url: function(url)
13492    {
13493        if (this.opts.linkValidation)
13494        {
13495            url = this._cleanUrl(url);
13496
13497            if (this._isMailto(url))
13498            {
13499                url = 'mailto:' + url.replace('mailto:', '');
13500            }
13501            else if (this._isUrl(url) && url.search(/^(ftp|https?)/i) === -1)
13502            {
13503                url = 'http://' + url.replace(/(ftp|https?):\/\//i, '');
13504            }
13505        }
13506
13507        this.attr('href', url);
13508    },
13509    _isMailto: function(url)
13510    {
13511        return (url.search('@') !== -1 && /(ftp|https?):\/\//i.test(url) === false);
13512    },
13513    _isUrl: function(url)
13514    {
13515        return this.reUrl.test(url);
13516    },
13517    _cleanUrl: function(url)
13518    {
13519        url = this._escapeHtml(url);
13520
13521        return url.trim().replace(/[^\W\w\D\d+&\'@#/%?=~_|!:,.;\(\)]/gi, '');
13522    },
13523    _escapeHtml: function(html) {
13524        return html.replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
13525    },
13526    _findDeepestChild: function(parent)
13527    {
13528        var result = {depth: 0, element: parent };
13529
13530        parent.children().each(function(node)
13531        {
13532            var child = $R.dom(node);
13533
13534            if (node.outerHTML !== parent.html())
13535            {
13536                return;
13537            }
13538            else
13539            {
13540                var childResult = this._findDeepestChild(child);
13541                if (childResult.depth + 1 > result.depth)
13542                {
13543                    result = {
13544                        depth: 1 + childResult.depth,
13545                        element: childResult.element
13546                    };
13547                }
13548            }
13549        }.bind(this));
13550
13551        return result;
13552    }
13553});
13554$R.add('module', 'modal', {
13555    init: function(app)
13556    {
13557        this.app = app;
13558        this.uuid = app.uuid;
13559        this.lang = app.lang;
13560        this.$doc = app.$doc;
13561        this.$win = app.$win;
13562        this.$body = app.$body;
13563        this.utils = app.utils;
13564        this.editor = app.editor;
13565        this.animate = app.animate;
13566        this.detector = app.detector;
13567        this.selection = app.selection;
13568
13569        // local
13570        this.$box = false;
13571        this.$modal = false;
13572        this.selectionMarkers = false;
13573
13574        // defaults
13575        this.defaults = {
13576            name: false,
13577            url: false,
13578            title: false,
13579            width: '600px',
13580            height: false,
13581            handle: false,
13582            commands: false
13583        };
13584    },
13585    // public
13586    build: function(data)
13587    {
13588        this._open(data);
13589    },
13590    close: function()
13591    {
13592        this._close();
13593    },
13594    onstop: function() {
13595        this.$body.find('#redactor-modal-' + this.uuid).remove();
13596        this.$body.find('#redactor-overlay-' + this.uuid).remove();
13597    },
13598    stop: function()
13599    {
13600        if (this.$box)
13601        {
13602            this.$box.remove();
13603            this.$box = false;
13604            this.$modal = false;
13605        }
13606
13607        if (this.$overlay) {
13608            this.$overlay.remove();
13609        }
13610
13611        this.$doc.off('.redactor.modal');
13612        this.$win.off('.redactor.modal');
13613    },
13614    resize: function()
13615    {
13616        this.$modal.setWidth(this.p.width);
13617        this.$modal.updatePosition();
13618    },
13619
13620    // private
13621    _isOpened: function()
13622    {
13623        return (this.$modal && this.$modal.hasClass('open'));
13624    },
13625    _open: function(data)
13626    {
13627        this._buildDefaults(data);
13628
13629        if (this.p.url) this._openUrl();
13630        else this._openTemplate();
13631    },
13632    _openUrl: function()
13633    {
13634        $R.ajax.post({
13635            url: this.p.url,
13636            success: this._doOpen.bind(this)
13637        });
13638    },
13639    _openTemplate: function()
13640    {
13641        if (typeof $R.modals[this.p.name] !== 'undefined')
13642        {
13643            var template = this.lang.parse($R.modals[this.p.name]);
13644            this._doOpen(template);
13645        }
13646    },
13647    _doOpen: function(template)
13648    {
13649        this.stop();
13650
13651        if (this.selection.isCollapsed())
13652        {
13653            this.selection.save();
13654            this.selectionMarkers = false;
13655        }
13656        else
13657        {
13658            this.selection.saveMarkers();
13659            this.selectionMarkers = true;
13660        }
13661
13662        if (!this.detector.isDesktop())
13663        {
13664            document.activeElement.blur();
13665        }
13666
13667        this._createModal(template);
13668
13669        this._buildModalBox();
13670        this._buildOverlay();
13671        this._buildModal();
13672        this._buildModalForm();
13673        this._buildModalCommands();
13674
13675        this._broadcast('open');
13676
13677        this.$modal.updatePosition();
13678        this._buildModalTabs();
13679
13680        this.animate.start(this.$box, 'fadeIn', this._opened.bind(this));
13681        this.animate.start(this.$overlay, 'fadeIn');
13682
13683    },
13684    _opened: function()
13685    {
13686        this.$modal.addClass('open');
13687        this.$box.on('mousedown.redactor.modal', this._close.bind(this));
13688        this.$doc.on('keyup.redactor.modal', this._handleEscape.bind(this));
13689        this.$win.on('resize.redactor.modal', this.resize.bind(this));
13690        this.$modal.getBody().find('input[type=text],input[type=url],input[type=email]').on('keydown.redactor.modal', this._handleEnter.bind(this));
13691
13692        // fix bootstrap modal focus
13693        if (window.jQuery) window.jQuery(document).off('focusin.modal');
13694
13695        this._broadcast('opened');
13696    },
13697    _close: function(e)
13698    {
13699        if (!this.$box || !this._isOpened()) return;
13700
13701        if (e)
13702        {
13703            if (!this._needToClose(e.target))
13704            {
13705                return;
13706            }
13707
13708            e.stopPropagation();
13709            e.preventDefault();
13710        }
13711
13712        if (this.selectionMarkers) this.selection.restoreMarkers();
13713        else this.selection.restore();
13714
13715
13716        this.selectionMarkers = false;
13717
13718        this._broadcast('close');
13719
13720        this.animate.start(this.$box, 'fadeOut', this._closed.bind(this));
13721        this.animate.start(this.$overlay, 'fadeOut');
13722    },
13723    _closed: function()
13724    {
13725        this.$modal.removeClass('open');
13726        this.$box.off('.redactor.modal');
13727        this.$doc.off('.redactor.modal');
13728        this.$win.off('.redactor.modal');
13729
13730        this._broadcast('closed');
13731    },
13732    _createModal: function(template)
13733    {
13734        this.$modal = $R.create('modal.element', this.app, template);
13735    },
13736    _broadcast: function(message)
13737    {
13738        this.app.broadcast('modal.' + message, this.$modal, this.$modalForm);
13739        this.app.broadcast('modal.' + this.p.name + '.' + message, this.$modal, this.$modalForm);
13740    },
13741    _buildDefaults: function(data)
13742    {
13743         this.p = $R.extend({}, this.defaults, data);
13744    },
13745    _buildModalBox: function()
13746    {
13747        this.$box = $R.dom('<div>');
13748        this.$box.attr('id', 'redactor-modal-' + this.uuid);
13749        this.$box.addClass('redactor-modal-box redactor-animate-hide');
13750        this.$box.html('');
13751        this.$body.append(this.$box);
13752    },
13753    _buildOverlay: function()
13754    {
13755        this.$overlay = $R.dom('#redactor-overlay-' + this.uuid);
13756        if (this.$overlay.length === 0)
13757        {
13758            this.$overlay = $R.dom('<div>');
13759            this.$overlay.attr('id', 'redactor-overlay-' + this.uuid);
13760            this.$overlay.addClass('redactor-overlay redactor-animate-hide');
13761            this.$body.prepend(this.$overlay);
13762        }
13763    },
13764    _buildModal: function()
13765    {
13766        this.$box.append(this.$modal);
13767
13768        this.$modal.setTitle(this.p.title);
13769        this.$modal.setHeight(this.p.height);
13770        this.$modal.setWidth(this.p.width);
13771    },
13772    _buildModalCommands: function()
13773    {
13774        if (this.p.commands)
13775        {
13776            var commands = this.p.commands;
13777            var $footer = this.$modal.getFooter();
13778            for (var key in commands)
13779            {
13780                var $btn = $R.dom('<button>');
13781
13782                $btn.html(commands[key].title);
13783                $btn.attr('data-command', key);
13784
13785                // cancel
13786                if (key === 'cancel')
13787                {
13788                    $btn.attr('data-action', 'close');
13789                    $btn.addClass('redactor-button-unstyled');
13790                }
13791
13792                // danger
13793                if (typeof commands[key].type !== 'undefined' && commands[key].type === 'danger')
13794                {
13795                    $btn.addClass('redactor-button-danger');
13796                }
13797
13798                $btn.on('click', this._handleCommand.bind(this));
13799
13800                $footer.append($btn);
13801            }
13802        }
13803    },
13804    _buildModalTabs: function()
13805    {
13806        var $body = this.$modal.getBody();
13807        var $tabs = $body.find('.redactor-modal-tab');
13808        var $box = $body.find('.redactor-modal-tabs');
13809
13810
13811        if ($tabs.length > 1)
13812        {
13813            $box = ($box.length === 0) ? $R.dom('<div>') : $box.html('');
13814            $box.addClass('redactor-modal-tabs');
13815
13816            $tabs.each(function(node, i)
13817            {
13818                var $node = $R.dom(node);
13819                var $item = $R.dom('<a>');
13820                $item.attr('href', '#');
13821                $item.attr('rel', i);
13822                $item.text($node.attr('data-title'));
13823                $item.on('click', this._showTab.bind(this));
13824
13825                if (i === 0)
13826                {
13827                    $item.addClass('active');
13828                }
13829
13830                $box.append($item);
13831
13832            }.bind(this));
13833
13834            $body.prepend($box);
13835        }
13836
13837        if ($tabs.length === 1) {
13838            $tabs.show();
13839        }
13840    },
13841    _buildModalForm: function()
13842    {
13843        this.$modalForm = $R.create('modal.form', this.app, this.$modal.getForm());
13844    },
13845    _showTab: function(e)
13846    {
13847        e.preventDefault();
13848
13849        var $el = $R.dom(e.target);
13850        var index = $el.attr('rel');
13851        var $body = this.$modal.getBody();
13852        var $tabs = $body.find('.redactor-modal-tab');
13853
13854        $tabs.hide();
13855        $tabs.eq(index).show();
13856
13857        $body.find('.redactor-modal-tabs a').removeClass('active');
13858        $el.addClass('active');
13859
13860    },
13861    _needToClose: function(el)
13862    {
13863        var $target = $R.dom(el);
13864        if ($target.attr('data-action') === 'close' || this.$modal.isCloseNode(el) || $target.closest('.redactor-modal').length === 0)
13865        {
13866            return true;
13867        }
13868
13869        return false;
13870    },
13871    _handleCommand: function(e)
13872    {
13873        var $btn = $R.dom(e.target).closest('button');
13874        var command = $btn.attr('data-command');
13875
13876        if (command !== 'cancel') e.preventDefault();
13877
13878        this._broadcast(command);
13879    },
13880    _handleEnter: function(e)
13881    {
13882        if (e.which === 13)
13883        {
13884            if (this.p.handle)
13885            {
13886                e.preventDefault();
13887                this._broadcast(this.p.handle);
13888            }
13889        }
13890    },
13891    _handleEscape: function(e)
13892    {
13893        if (e.which === 27) this._close();
13894    }
13895});
13896$R.add('class', 'modal.element', {
13897    mixins: ['dom'],
13898    init: function(app, template)
13899    {
13900        this.app = app;
13901        this.opts = app.opts;
13902        this.$win = app.$win;
13903
13904        // init
13905        this._init(template);
13906    },
13907
13908    // get
13909    getForm: function()
13910    {
13911        return this.find('form');
13912    },
13913    getHeader: function()
13914    {
13915        return this.$modalHeader;
13916    },
13917    getBody: function()
13918    {
13919        return this.$modalBody;
13920    },
13921    getFooter: function()
13922    {
13923        return this.$modalFooter;
13924    },
13925
13926    // set
13927    setTitle: function(title)
13928    {
13929        if (title) this.$modalHeader.html(title);
13930    },
13931    setWidth: function(width)
13932    {
13933        width = (parseInt(width) >= this.$win.width()) ? '96%' : width;
13934
13935        this.css('max-width', width);
13936    },
13937    setHeight: function(height)
13938    {
13939        if (height !== false) this.$modalBody.css('height', height);
13940    },
13941
13942    // update
13943    updatePosition: function()
13944    {
13945        var width = this.width();
13946        this.css({ 'left': '50%', 'margin-left': '-' + (width/2) + 'px' });
13947
13948        var windowHeight = this.$win.height();
13949        var height = this.height();
13950        var marginTop = (windowHeight/2 - height/2);
13951
13952        if (height < windowHeight && marginTop !== 0)
13953        {
13954            this.css('margin-top', marginTop + 'px');
13955        }
13956    },
13957
13958    // is
13959    isCloseNode: function(el)
13960    {
13961        return (el === this.$modalClose.get());
13962    },
13963
13964    // private
13965    _init: function(template)
13966    {
13967        this._build();
13968        this._buildClose();
13969        this._buildHeader();
13970        this._buildBody();
13971        this._buildFooter();
13972        this._buildTemplate(template);
13973    },
13974    _build: function()
13975    {
13976        this.parse('<div>');
13977        this.addClass('redactor-modal');
13978        this.attr('dir', this.opts.direction);
13979    },
13980    _buildClose: function()
13981    {
13982        this.$modalClose = $R.dom('<span>');
13983        this.$modalClose.addClass('redactor-close');
13984
13985        this.append(this.$modalClose);
13986    },
13987    _buildHeader: function()
13988    {
13989        this.$modalHeader = $R.dom('<div>');
13990        this.$modalHeader.addClass('redactor-modal-header');
13991
13992        this.append(this.$modalHeader);
13993    },
13994    _buildBody: function()
13995    {
13996        this.$modalBody = $R.dom('<div>');
13997        this.$modalBody.addClass('redactor-modal-body');
13998
13999        this.append(this.$modalBody);
14000    },
14001    _buildFooter: function()
14002    {
14003        this.$modalFooter = $R.dom('<div>');
14004        this.$modalFooter.addClass('redactor-modal-footer');
14005
14006        this.append(this.$modalFooter);
14007    },
14008    _buildTemplate: function(template)
14009    {
14010        this.$modalBody.html(template);
14011    }
14012});
14013$R.add('class', 'modal.form', {
14014    mixins: ['dom'],
14015    init: function(app, element)
14016    {
14017        this.app = app;
14018
14019        // build
14020        this.build(element);
14021    },
14022
14023    // public
14024    build: function(element)
14025    {
14026        this.parse(element);
14027    },
14028    getData: function()
14029    {
14030        var data = {};
14031        this.find('[name]').each(function(node)
14032        {
14033            var $node = $R.dom(node);
14034            data[$node.attr('name')] = $node.val();
14035        });
14036
14037        return data;
14038    },
14039    setData: function(data)
14040    {
14041        this.find('[name]').each(function(node)
14042        {
14043            var $node = $R.dom(node);
14044            var name = $node.attr('name');
14045            if (data.hasOwnProperty(name))
14046            {
14047                if (node.type && node.type === 'checkbox') node.checked = data[name];
14048                else $node.val(data[name]);
14049            }
14050        });
14051    },
14052    getField: function(name)
14053    {
14054        return this.find('[name=' + name + ']');
14055    },
14056    setError: function(name)
14057    {
14058        var $el = this.getField(name);
14059
14060        $el.addClass('error');
14061        $el.one(this._getFieldEventName($el.get()), this._clearError);
14062
14063        return false;
14064    },
14065
14066    // private
14067    _clearError: function()
14068    {
14069        return $R.dom(this).removeClass('error');
14070    },
14071    _getFieldEventName: function(el)
14072    {
14073        return (el.tagName === 'SELECT' || el.type === 'checkbox' || el.type === 'radio') ? 'change' : 'keyup';
14074    }
14075});
14076$R.add('module', 'block', {
14077    init: function(app)
14078    {
14079        this.app = app;
14080        this.block = app.block;
14081    },
14082    // public
14083    format: function(args)
14084    {
14085        var nodes = this.block.format(args);
14086
14087        // callback
14088        this.app.broadcast('format', 'block', nodes);
14089    },
14090    clearformat: function()
14091    {
14092        this.block.clearFormat();
14093    },
14094    clearstyle: function()
14095    {
14096        this.block.clearStyle();
14097    },
14098    clearclass: function()
14099    {
14100        this.block.clearClass();
14101    },
14102    clearattr: function()
14103    {
14104        this.block.clearAttr();
14105    },
14106    add: function(args, tags)
14107    {
14108        this.block.add(args, tags);
14109    },
14110    toggle: function(args, tags)
14111    {
14112        this.block.toggle(args, tags);
14113    },
14114    set: function(args, tags)
14115    {
14116        this.block.set(args, tags);
14117    },
14118    remove: function(args, tags)
14119    {
14120        this.block.remove(args, tags);
14121    }
14122});
14123$R.add('module', 'inline', {
14124    init: function(app)
14125    {
14126        this.app = app;
14127        this.inline = app.inline;
14128    },
14129    format: function(args)
14130    {
14131        var nodes = this.inline.format(args);
14132
14133        // callback
14134        this.app.broadcast('format', 'inline', nodes);
14135    },
14136    clearformat: function()
14137    {
14138        this.inline.clearFormat();
14139    },
14140    clearstyle: function()
14141    {
14142        this.inline.clearStyle();
14143    },
14144    clearclass: function()
14145    {
14146        this.inline.clearClass();
14147    },
14148    clearattr: function()
14149    {
14150        this.inline.clearAttr();
14151    },
14152    add: function(args, tags)
14153    {
14154        this.inline.add(args, tags);
14155    },
14156    toggle: function(args, tags)
14157    {
14158        this.inline.toggle(args, tags);
14159    },
14160    set: function(args, tags)
14161    {
14162        this.inline.set(args, tags);
14163    },
14164    remove: function(args, tags)
14165    {
14166        this.inline.remove(args, tags);
14167    }
14168});
14169$R.add('module', 'autosave', {
14170    init: function(app)
14171    {
14172        this.app = app;
14173        this.opts = app.opts;
14174        this.utils = app.utils;
14175        this.source = app.source;
14176    },
14177    // messages
14178    onsynced: function()
14179    {
14180        if (this.opts.autosave)
14181        {
14182            this._send();
14183        }
14184    },
14185
14186    // private
14187    _send: function()
14188    {
14189        var name = (this.opts.autosaveName) ? this.opts.autosaveName : this.source.getName();
14190
14191        var data = {};
14192        data[name] = this.source.getCode();
14193        data = this.utils.extendData(data, this.opts.autosaveData);
14194        $R.ajax.request(this.opts.autosaveMethod, {
14195            url: this.opts.autosave,
14196            data: data,
14197            success: function(response)
14198            {
14199                this._complete(response, name, data);
14200            }.bind(this)
14201        });
14202    },
14203    _complete: function(response, name, data)
14204    {
14205        var callback = (response && response.error) ? 'autosaveError' : 'autosave';
14206        this.app.broadcast(callback, name, data, response);
14207    }
14208});
14209$R.add('module', 'input', {
14210    init: function(app)
14211    {
14212        this.app = app;
14213        this.opts = app.opts;
14214        this.utils = app.utils;
14215        this.editor = app.editor;
14216        this.keycodes = app.keycodes;
14217        this.element = app.element;
14218        this.selection = app.selection;
14219        this.insertion = app.insertion;
14220        this.inspector = app.inspector;
14221        this.autoparser = app.autoparser;
14222
14223        // local
14224        this.lastShiftKey = false;
14225    },
14226    // messages
14227    onpaste: function(e, dataTransfer)
14228    {
14229        if (!this.opts.input) return;
14230
14231        return $R.create('input.paste', this.app, e, dataTransfer);
14232    },
14233    onkeydown: function(e)
14234    {
14235        if (!this.opts.input) return;
14236
14237        // key
14238        var key = e.which;
14239
14240        // shortcuts
14241        var shortcut = $R.create('input.shortcut', this.app, e);
14242        if (shortcut.is()) return;
14243
14244        // select all
14245        if ((e.ctrlKey || e.metaKey) && !e.altKey && key === 65)
14246        {
14247            e.preventDefault();
14248            return this._selectAll();
14249        }
14250
14251        // set empty if all selected
14252        var keys = [this.keycodes.ENTER, this.keycodes.SPACE, this.keycodes.BACKSPACE, this.keycodes.DELETE];
14253        var arrowKeys = [this.keycodes.UP, this.keycodes.DOWN, this.keycodes.LEFT, this.keycodes.RIGHT];
14254        var isKeys = (keys.indexOf(key) !== -1);
14255        var isArrowKeys = (arrowKeys.indexOf(key) !== -1);
14256        var isXKey = ((e.ctrlKey || e.metaKey) && key === 88); // x
14257        var isAlphaKeys = ((!e.ctrlKey && !e.metaKey) && ((key >= 48 && key <= 57) || (key >= 65 && key <= 90)));
14258
14259        if (this.selection.isAll() && isArrowKeys && (isXKey || (!e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey)))
14260        {
14261            if (isXKey)
14262            {
14263                this.editor.disableNonEditables();
14264                this.app.broadcast('empty');
14265                return;
14266            }
14267
14268            if (this._isArrowKey(key)) return true;
14269            if (isKeys) e.preventDefault();
14270
14271            if (this.element.isType('inline'))
14272            {
14273                var $editor = this.editor.getElement();
14274                $editor.html('');
14275
14276                this.editor.startFocus();
14277            }
14278            else
14279            {
14280                this.insertion.set(this.opts.emptyHtml);
14281            }
14282
14283            if (isKeys) return;
14284            else this.app.broadcast('empty');
14285        }
14286
14287        // autoparse
14288        if (this.opts.autoparse)
14289        {
14290            this.autoparser.format(e, key);
14291        }
14292
14293        // a-z, 0-9 - non editable
14294        if (isAlphaKeys)
14295        {
14296            // has non-editable
14297            if (this.selection.hasNonEditable())
14298            {
14299                e.preventDefault();
14300                return;
14301            }
14302        }
14303
14304        // enter, shift/ctrl + enter
14305        if (key === this.keycodes.ENTER)
14306        {
14307            return $R.create('input.enter', this.app, e, key);
14308        }
14309        // cmd + [
14310        else if (e.metaKey && key === 219)
14311        {
14312            e.preventDefault();
14313            this.app.api('module.list.outdent');
14314            return;
14315        }
14316        // tab or cmd + ]
14317        else if (key === this.keycodes.TAB || (e.metaKey && key === 221))
14318        {
14319            return $R.create('input.tab', this.app, e, key);
14320        }
14321        // space
14322        else if (key === this.keycodes.SPACE)
14323        {
14324            return $R.create('input.space', this.app, e, key, this.lastShiftKey);
14325        }
14326        // backspace or delete
14327        else if (this._isDeleteKey(key))
14328        {
14329            return $R.create('input.delete', this.app, e, key);
14330        }
14331        else if (this._isArrowKey(key))
14332        {
14333            return $R.create('input.arrow', this.app, e, key);
14334        }
14335    },
14336    onkeyup: function(e)
14337    {
14338        if (!this.opts.input) return;
14339
14340        // key
14341        var key = e.which;
14342
14343        // shift key
14344        this.lastShiftKey = e.shiftKey;
14345
14346        // hide context toolbar
14347        this.app.broadcast('contextbar.close');
14348
14349        // shortcode
14350        var shortcode = $R.create('input.shortcode', this.app, e, key);
14351        if (shortcode.is()) return;
14352
14353        // is empty
14354        if (key === this.keycodes.BACKSPACE)
14355        {
14356            var $editor = this.editor.getElement();
14357            var html = this.utils.trimSpaces($editor.html());
14358            html = html.replace(/<br\s?\/?>/g, '');
14359            html = html.replace(/<div><\/div>/, '');
14360
14361            if (html === '')
14362            {
14363                e.preventDefault();
14364                this.editor.setEmpty();
14365                this.editor.startFocus();
14366                return;
14367            }
14368        }
14369
14370        if (this.editor.isEmpty())
14371        {
14372            this.app.broadcast('empty');
14373        }
14374    },
14375
14376    // public
14377    start: function()
14378    {
14379        // extend shortcuts
14380        if (this.opts.shortcutsAdd)
14381        {
14382            this.opts.shortcuts = $R.extend({}, true, this.opts.shortcuts, this.opts.shortcutsAdd);
14383        }
14384    },
14385
14386    // private
14387    _selectAll: function()
14388    {
14389        var current = this.selection.getCurrent();
14390        var data = this.inspector.parse(current);
14391        var el;
14392
14393        if (data.isComponentType('table'))
14394        {
14395            el = data.getTable();
14396            this.selection.setAll(el);
14397            return;
14398        }
14399        else if (data.isComponentType('code'))
14400        {
14401            el = data.getComponentCodeElement();
14402            this.selection.setAll(el);
14403            return;
14404        }
14405
14406        this.selection.setAll();
14407    },
14408    _isArrowKey: function(key)
14409    {
14410        return ([this.keycodes.UP, this.keycodes.DOWN, this.keycodes.RIGHT, this.keycodes.LEFT].indexOf(key) !== -1);
14411    },
14412    _isDeleteKey: function(key)
14413    {
14414        return (key === this.keycodes.BACKSPACE || key === this.keycodes.DELETE);
14415    }
14416});
14417$R.add('class', 'input.arrow', {
14418    init: function(app, e, key)
14419    {
14420        this.app = app;
14421        this.opts = app.opts;
14422        this.utils = app.utils;
14423        this.caret = app.caret;
14424        this.offset = app.offset;
14425        this.marker = app.marker;
14426        this.editor = app.editor;
14427        this.keycodes = app.keycodes;
14428        this.component = app.component;
14429        this.inspector = app.inspector;
14430        this.selection = app.selection;
14431
14432        // local
14433        this.key = key;
14434
14435        // init
14436        this._init(e);
14437    },
14438    // private
14439    _init: function(e)
14440    {
14441        if (this._isRightLeftKey() && this._isExitVariable(e)) return;
14442
14443        if (this._isRightDownKey())
14444        {
14445            if (this._isExitOnDownRight(e)) return;
14446            if (this._selectComponent(e, 'End', 'next')) return;
14447        }
14448
14449        if (this._isLeftUpKey())
14450        {
14451            if (this._isExitOnUpLeft(e)) return;
14452            if (this._selectComponent(e, 'Start', 'prev')) return;
14453        }
14454
14455        if (this.key === this.keycodes.LEFT) this.utils.trimInvisibleChars('left');
14456        else if (this.key === this.keycodes.RIGHT) this.utils.trimInvisibleChars('right');
14457
14458    },
14459    _isRightDownKey: function()
14460    {
14461        return ([this.keycodes.DOWN, this.keycodes.RIGHT].indexOf(this.key) !== -1);
14462    },
14463    _isLeftUpKey: function()
14464    {
14465        return ([this.keycodes.UP, this.keycodes.LEFT].indexOf(this.key) !== -1);
14466    },
14467    _isRightLeftKey: function()
14468    {
14469        return ([this.keycodes.RIGHT, this.keycodes.LEFT].indexOf(this.key) !== -1);
14470    },
14471    _isExitVariable: function(e)
14472    {
14473        var current = this.selection.getCurrent();
14474        var data = this.inspector.parse(current);
14475        var component = data.getComponent();
14476        if (data.isComponentType('variable') && data.isComponentActive())
14477        {
14478            e.preventDefault();
14479            var func = (this.key === this.keycodes.LEFT) ? 'setBefore' : 'setAfter';
14480            this.caret[func](component);
14481            return;
14482        }
14483    },
14484    _isExitOnUpLeft: function(e)
14485    {
14486        var current = this.selection.getCurrent();
14487        var block = this.selection.getBlock(current);
14488        var data = this.inspector.parse(current);
14489        var prev = block.previousElementSibling;
14490        var isStart = this.caret.isStart(block);
14491
14492        // prev table
14493        if (isStart && prev && prev.tagName === 'TABLE')
14494        {
14495            e.preventDefault();
14496            this.caret.setEnd(prev);
14497            return true;
14498        }
14499        // figcaption
14500        else if (data.isFigcaption())
14501        {
14502            block = data.getFigcaption();
14503            isStart = this.caret.isStart(block);
14504
14505            var $component = $R.dom(block).closest('.redactor-component');
14506            if (isStart && $component.length !== 0)
14507            {
14508                e.preventDefault();
14509                this.caret.setEnd($component);
14510                return true;
14511            }
14512        }
14513        // exit table
14514        else if (data.isTable() && isStart)
14515        {
14516            e.preventDefault();
14517            this.caret.setEnd(block.previousElementSibling);
14518            return true;
14519        }
14520        // component
14521        else if (!data.isComponentEditable() && data.isComponent() && !data.isComponentType('variable'))
14522        {
14523            var component = data.getComponent();
14524            if (!component.previousElementSibling)
14525            {
14526                e.preventDefault();
14527                this.component.clearActive();
14528
14529                return this._exitPrevElement(e, data.getComponent());
14530            }
14531            else if (component.previousElementSibling)
14532            {
14533                e.preventDefault();
14534                this.component.clearActive();
14535                this.caret.setEnd(component.previousElementSibling);
14536                return true;
14537            }
14538        }
14539    },
14540    _isExitOnDownRight: function(e)
14541    {
14542        var $editor = this.editor.getElement();
14543        var current = this.selection.getCurrent();
14544        var data = this.inspector.parse(current);
14545        var isEndEditor = this.caret.isEnd();
14546        var block, isEnd;
14547
14548        // table
14549        if (data.isTable())
14550        {
14551            block = data.getTable();
14552            isEnd = this.caret.isEnd(block);
14553
14554            if (isEnd || isEndEditor)
14555            {
14556                return this._exitNextElement(e, data.getComponent());
14557            }
14558        }
14559        // figcaption
14560        else if (data.isFigcaption())
14561        {
14562            block = data.getFigcaption();
14563            isEnd = this.caret.isEnd(block);
14564
14565            if (isEnd || isEndEditor)
14566            {
14567                return this._exitNextElement(e, data.getComponent());
14568            }
14569        }
14570        // figure/code
14571        else if (data.isComponentType('code'))
14572        {
14573            var component = data.getComponent();
14574            var pre = $R.dom(data.getComponentCodeElement()).closest('pre');
14575
14576            isEnd = this.caret.isEnd(block);
14577
14578            var isNext = (pre && pre.get().nextElementSibling);
14579            if (isEnd && !isNext)
14580            {
14581                return this._exitNextElement(e, component);
14582            }
14583        }
14584        // pre & blockquote & dl
14585        else if (data.isPre() || data.isBlockquote() || data.isDl())
14586        {
14587            if (isEndEditor)
14588            {
14589                if (data.isPre()) return this._exitNextElement(e, data.getPre());
14590                else if (data.isBlockquote()) return this._exitNextElement(e, data.getBlockquote());
14591                else if (data.isDl()) return this._exitNextElement(e, data.getDl());
14592            }
14593        }
14594        // li
14595        else if (data.isList())
14596        {
14597            var $list = $R.dom(current).parents('ul, ol', $editor).last();
14598            isEnd = this.caret.isEnd($list);
14599
14600            if (isEnd || isEndEditor)
14601            {
14602                return this._exitNextElement(e, $list.get());
14603            }
14604        }
14605        // component
14606        else if (data.isComponent() && !data.isComponentType('variable') && data.getTag() !== 'span')
14607        {
14608            this.component.clearActive();
14609            return this._exitNextElement(e, data.getComponent());
14610        }
14611    },
14612    _exitPrevElement: function(e, node)
14613    {
14614        e.preventDefault();
14615
14616        if (node.previousElementSibling) this.caret.setEnd(node.previousElementSibling);
14617        else this.utils.createMarkupBefore(node);
14618
14619        return true;
14620    },
14621    _exitNextElement: function(e, node)
14622    {
14623        e.preventDefault();
14624
14625        if (node.nextElementSibling) this.caret.setStart(node.nextElementSibling);
14626        else this.utils.createMarkup(node);
14627
14628        return true;
14629    },
14630    _selectComponent: function(e, caret, type)
14631    {
14632        var current = this.selection.getCurrent();
14633        var block = this.selection.getBlock(current);
14634        var sibling = this.utils.findSiblings(current, type);
14635        var siblingBlock = this.utils.findSiblings(block, type);
14636
14637        if (sibling && this.caret['is' + caret](current))
14638        {
14639            this._selectComponentItem(e, sibling, caret);
14640        }
14641        else if (siblingBlock && this.caret['is' + caret](block))
14642        {
14643            this._selectComponentItem(e, siblingBlock, caret);
14644        }
14645    },
14646    _selectComponentItem: function(e, item, caret)
14647    {
14648        if (this.component.isNonEditable(item))
14649        {
14650            e.preventDefault();
14651            this.caret['set' + caret](item);
14652            return true;
14653        }
14654    }
14655});
14656$R.add('class', 'input.delete', {
14657    init: function(app, e, key)
14658    {
14659        this.app = app;
14660        this.opts = app.opts;
14661        this.caret = app.caret;
14662        this.utils = app.utils;
14663        this.editor = app.editor;
14664        this.marker = app.marker;
14665        this.keycodes = app.keycodes;
14666        this.component = app.component;
14667        this.inspector = app.inspector;
14668        this.selection = app.selection;
14669        this.insertion = app.insertion;
14670
14671        // local
14672        this.key = key;
14673
14674        // init
14675        this._init(e);
14676    },
14677    // private
14678    _init: function(e)
14679    {
14680        if (this._removeActiveComponent(e)) return;
14681        if (this._removeAllSelectedTable(e)) return;
14682
14683        // is empty
14684        if (this.key === this.keycodes.BACKSPACE)
14685        {
14686            var $editor = this.editor.getElement();
14687            var html = this.utils.trimSpaces($editor.html());
14688
14689            if (html === this.opts.emptyHtml)
14690            {
14691                e.preventDefault();
14692                return;
14693            }
14694        }
14695
14696        // variable or non editable prev/next or selection
14697        if (this._detectVariableOrNonEditable() || this.selection.hasNonEditable())
14698        {
14699            e.preventDefault();
14700            return;
14701        }
14702
14703        // all selected
14704        if (this.selection.isAll())
14705        {
14706            e.preventDefault();
14707            this.insertion.set(this.opts.emptyHtml);
14708            return;
14709        }
14710
14711        // collapsed
14712        if (this.selection.isCollapsed())
14713        {
14714            // next / prev
14715            if (this.key === this.keycodes.BACKSPACE) this._traverseBackspace(e);
14716            else if (this.key === this.keycodes.DELETE) this._traverseDelete(e);
14717        }
14718
14719        if (this.key === this.keycodes.BACKSPACE) this.utils.trimInvisibleChars('left');
14720
14721        this._removeUnwantedStyles();
14722        this._removeEmptySpans();
14723        this._removeSpanTagsInHeadings();
14724        this._removeInlineTagsInPre();
14725    },
14726    _detectVariableOrNonEditable: function()
14727    {
14728        var block = this.selection.getBlock();
14729        var isBlockStart = this.caret.isStart(block);
14730        var isBlockEnd = this.caret.isEnd(block);
14731        var el;
14732
14733        // backspace
14734        if (this.key === this.keycodes.BACKSPACE && isBlockStart)
14735        {
14736            el = block.previousSibling;
14737            if (this._isNonEditable(el)) return true;
14738        }
14739        // delete
14740        else if (this.key === this.keycodes.DELETE && isBlockEnd)
14741        {
14742            el = block.nextSibling;
14743            if (this._isNonEditable(el)) return true;
14744        }
14745
14746        var current = this.selection.getCurrent();
14747        var isCurrentStart = this.caret.isStart(current);
14748        var isCurrentEnd = this.caret.isEnd(current);
14749        var isCurrentStartSpace = (this.selection.getTextBeforeCaret().trim() === '');
14750        var isCurrentEndSpace = (this.selection.getTextAfterCaret().trim() === '');
14751
14752        // backspace
14753        if (this.key === this.keycodes.BACKSPACE && isCurrentStart && !isCurrentStartSpace)
14754        {
14755            el = current.previousSibling;
14756            if (this._isVariable(el))
14757            {
14758                this.caret.setEnd(el);
14759                return true;
14760            }
14761            else if (this._isNonEditable(el)) return true;
14762        }
14763        // delete
14764        else if (this.key === this.keycodes.DELETE && isCurrentEnd && !isCurrentEndSpace)
14765        {
14766            el = current.nextSibling;
14767            if (this._isVariable(el))
14768            {
14769                this.caret.setStart(el);
14770                return true;
14771            }
14772            else if (this._isNonEditable(el)) return true;
14773        }
14774    },
14775    _isVariable: function(node)
14776    {
14777        return ($R.dom(node).closest('[data-redactor-type="variable"]').length !== 0);
14778    },
14779    _isNonEditable: function(node)
14780    {
14781        return ($R.dom(node).closest('.non-editable').length !== 0);
14782    },
14783    _getBlock: function()
14784    {
14785        var $editor = this.editor.getElement();
14786        var block = this.selection.getBlock();
14787        var data = this.inspector.parse(block);
14788
14789        block = (data.isList()) ? $R.dom(block).parents('ul, ol', $editor).last().get() : block;
14790        block = (data.isDl()) ? data.getDl() : block;
14791        block = (data.isTable()) ? data.getTable() : block;
14792
14793        return block;
14794    },
14795    _traverseDelete: function(e)
14796    {
14797        var current = this.selection.getCurrent();
14798        var data = this.inspector.parse(current);
14799        var block, isEnd, $next;
14800
14801        // figcaption
14802        if (data.isFigcaption())
14803        {
14804            block = data.getFigcaption();
14805            isEnd = this.caret.isEnd(block);
14806
14807            if (isEnd)
14808            {
14809                e.preventDefault();
14810                return;
14811            }
14812        }
14813        // figure/code
14814        else if (data.isComponentType('code'))
14815        {
14816            block = data.getComponent();
14817            isEnd = this.caret.isEnd(block);
14818
14819            if (isEnd)
14820            {
14821                e.preventDefault();
14822                return;
14823            }
14824        }
14825
14826
14827        // next
14828        block = this._getBlock();
14829        var next = this.utils.findSiblings(block, 'next');
14830        if (!next) return;
14831
14832        isEnd = this.caret.isEnd(block);
14833        var dataNext = this.inspector.parse(next);
14834        var isNextBlock = (next.tagName === 'P' || next.tagName === 'DIV');
14835
14836        // table
14837        if (isEnd && dataNext.isComponentType('table'))
14838        {
14839            e.preventDefault();
14840            this.caret.setStart(next);
14841            return;
14842        }
14843        // figure/code
14844        else if (isEnd && dataNext.isComponentEditable())
14845        {
14846            e.preventDefault();
14847            this.component.remove(next, false);
14848            return;
14849        }
14850        // component
14851        else if (isEnd && dataNext.isComponent())
14852        {
14853            e.preventDefault();
14854
14855            // select component
14856            this.caret.setStart(next);
14857
14858            // remove current if empty
14859            if (this.utils.isEmptyHtml(block.innerHTML))
14860            {
14861                $R.dom(block).remove();
14862            }
14863
14864            return;
14865        }
14866        // combine list
14867        else if (isEnd && dataNext.isList())
14868        {
14869            var $currentList = $R.dom(block);
14870            $next = $R.dom(next);
14871
14872            // current list
14873            if (data.isList())
14874            {
14875                e.preventDefault();
14876
14877                $currentList.append($next);
14878                $next.unwrap();
14879
14880                return;
14881            }
14882            else
14883            {
14884                var $first = $next.children('li').first();
14885                var $lists = $first.find('ul, ol');
14886
14887                if ($lists.length !== 0)
14888                {
14889                    e.preventDefault();
14890
14891                    $next.prepend($lists);
14892                    $lists.unwrap();
14893
14894                    $currentList.append($first);
14895                    $first.unwrap();
14896
14897                    return;
14898                }
14899            }
14900        }
14901        // block
14902        else if (isEnd && !data.isList() && !data.isTable() && isNextBlock && !this.utils.isEmptyHtml(block.innerHTML))
14903        {
14904            e.preventDefault();
14905
14906            var $current = $R.dom(block);
14907            $next = $R.dom(next);
14908
14909            $current.append($next);
14910            $next.unwrap();
14911
14912            return;
14913        }
14914    },
14915    _traverseBackspace: function(e)
14916    {
14917        var current = this.selection.getCurrent();
14918        var data = this.inspector.parse(current);
14919        var block, isStart, $prev, $currentList;
14920
14921        // figcaption
14922        if (data.isFigcaption())
14923        {
14924            block = data.getFigcaption();
14925            isStart = this.caret.isStart(block);
14926
14927            if (isStart)
14928            {
14929                e.preventDefault();
14930                return;
14931            }
14932        }
14933        // figure/code
14934        else if (data.isComponentType('code'))
14935        {
14936            block = data.getComponent();
14937            isStart = this.caret.isStart(block);
14938
14939            if (isStart && block.previousElementSibling)
14940            {
14941                e.preventDefault();
14942                this.caret.setEnd(block.previousElementSibling);
14943                return true;
14944            }
14945        }
14946
14947        // prev
14948        block = this._getBlock();
14949        var prev = this.utils.findSiblings(block, 'prev');
14950
14951        if (!prev)
14952        {
14953            setTimeout(this._replaceBlock.bind(this), 1);
14954            return;
14955        }
14956
14957        isStart = this.caret.isStart(block);
14958        var dataPrev = this.inspector.parse(prev);
14959        var isPrevBlock = (prev.tagName === 'P' || prev.tagName === 'DIV');
14960
14961        // figure/code
14962        if (isStart && dataPrev.isComponentType('code'))
14963        {
14964            e.preventDefault();
14965            this.component.remove(prev, false);
14966            return;
14967        }
14968        // table
14969        else if (isStart && dataPrev.isComponentType('table'))
14970        {
14971            e.preventDefault();
14972            this.caret.setEnd(prev);
14973            return;
14974        }
14975        // component
14976        else if (isStart && dataPrev.isComponent())
14977        {
14978            e.preventDefault();
14979
14980            // select component
14981            this.caret.setStart(prev);
14982
14983
14984            // remove current if empty
14985            if (this.utils.isEmptyHtml(block.innerHTML))
14986            {
14987                $R.dom(block).remove();
14988            }
14989
14990            return;
14991        }
14992        // lists
14993        else if (isStart && data.isList())
14994        {
14995            e.preventDefault();
14996
14997            $currentList = $R.dom(block);
14998            $prev = $R.dom(prev);
14999
15000            if (dataPrev.isList())
15001            {
15002                $currentList.children('li').first().prepend(this.marker.build('start'));
15003                $prev.append($currentList);
15004                $currentList.unwrap();
15005
15006                this.selection.restoreMarkers();
15007            }
15008            else
15009            {
15010                var $first = $currentList.children('li').first();
15011                var first = $first.get();
15012                var $lists = $first.find('ul, ol');
15013
15014                var $newnode = this.utils.replaceToTag(first, this.opts.markup);
15015                if (this.opts.breakline) $newnode.attr('data-redactor-tag', 'br');
15016                $currentList.before($newnode);
15017                this.caret.setStart($newnode);
15018
15019                if ($lists.length !== 0)
15020                {
15021                    $currentList.prepend($lists);
15022                    $lists.unwrap();
15023                }
15024            }
15025
15026            return;
15027        }
15028        // block
15029        else if (isStart && isPrevBlock)
15030        {
15031            e.preventDefault();
15032
15033            if (this.utils.isEmpty(prev)) {
15034                $prev = $R.dom(prev);
15035                $prev.remove();
15036                return;
15037            }
15038
15039            var textNode = this.utils.createInvisibleChar();
15040            var $current = $R.dom(block);
15041            $prev = $R.dom(prev);
15042
15043            this.caret.setEnd($prev);
15044
15045            $current.prepend(textNode);
15046            $prev.append($current.contents());
15047            $current.remove();
15048
15049            return;
15050        }
15051
15052    },
15053    _replaceBlock: function()
15054    {
15055        var block = this.selection.getBlock();
15056        var $block = $R.dom(block);
15057
15058        if (this.opts.markup === 'p' && block && this._isNeedToReplaceBlock(block))
15059        {
15060            var markup = document.createElement(this.opts.markup);
15061
15062            $block.replaceWith(markup);
15063            this.caret.setStart(markup);
15064        }
15065
15066        if (this.opts.breakline && block && block.tagName === 'DIV')
15067        {
15068            $block.attr('data-redactor-tag', 'br');
15069        }
15070    },
15071    _isNeedToReplaceBlock: function(block)
15072    {
15073        return (block.tagName === 'DIV' && this.utils.isEmptyHtml(block.innerHTML));
15074    },
15075    _removeActiveComponent: function(e)
15076    {
15077        var current = this.selection.getCurrent();
15078        var data = this.inspector.parse(current);
15079        var component = data.getComponent();
15080        if (data.isComponent() && this.component.isActive(component))
15081        {
15082            e.preventDefault();
15083            this.component.remove(component);
15084            return true;
15085        }
15086    },
15087    _removeAllSelectedTable: function(e)
15088    {
15089        var current = this.selection.getCurrent();
15090        var data = this.inspector.parse(current);
15091        var table = data.getTable();
15092        if (table && this.selection.isAll(table))
15093        {
15094            e.preventDefault();
15095            this.component.remove(table);
15096            return true;
15097        }
15098    },
15099    _removeUnwantedStyles: function()
15100    {
15101        var $editor = this.editor.getElement();
15102
15103        setTimeout(function()
15104        {
15105            var $tags = $editor.find('*[style]');
15106            $tags.not('img, figure, figcaption, iframe, [data-redactor-style-cache], [data-redactor-span]').removeAttr('style');
15107
15108        }, 0);
15109    },
15110    _removeEmptySpans: function()
15111    {
15112        var $editor = this.editor.getElement();
15113
15114        setTimeout(function()
15115        {
15116            $editor.find('span').each(function(node)
15117            {
15118                if (node.attributes.length === 0)
15119                {
15120                    $R.dom(node).replaceWith(node.childNodes);
15121                }
15122            });
15123
15124        }, 0);
15125    },
15126    _removeSpanTagsInHeadings: function()
15127    {
15128        var $editor = this.editor.getElement();
15129
15130        setTimeout(function()
15131        {
15132            $editor.find('h1, h2, h3, h4, h5, h6').each(function(node)
15133            {
15134                var $node = $R.dom(node);
15135                if ($node.closest('figure').length === 0)
15136                {
15137                    $node.find('span').not('.redactor-component, .non-editable, .redactor-selection-marker, [data-redactor-style-cache], [data-redactor-span]').unwrap();
15138                }
15139            });
15140
15141        }, 1);
15142    },
15143    _removeInlineTagsInPre: function()
15144    {
15145        var $editor = this.editor.getElement();
15146        var tags = this.opts.inlineTags;
15147
15148        setTimeout(function()
15149        {
15150            $editor.find('pre').each(function(node)
15151            {
15152                var $node = $R.dom(node);
15153                if ($node.closest('figure').length === 0)
15154                {
15155                    $node.find(tags.join(',')).not('code, .redactor-selection-marker').unwrap();
15156                }
15157            });
15158
15159        }, 1);
15160    }
15161});
15162$R.add('class', 'input.enter', {
15163    init: function(app, e)
15164    {
15165        this.app = app;
15166        this.opts = app.opts;
15167        this.utils = app.utils;
15168        this.caret = app.caret;
15169        this.editor = app.editor;
15170        this.keycodes = app.keycodes;
15171        this.detector = app.detector;
15172        this.insertion = app.insertion;
15173        this.selection = app.selection;
15174        this.inspector = app.inspector;
15175
15176        // init
15177        this._init(e);
15178    },
15179    // private
15180    _init: function(e)
15181    {
15182        // turn off
15183        if (!this.opts.enterKey) return this._disable(e);
15184
15185        // callback
15186        var stop = this.app.broadcast('enter', e);
15187        if (stop === false) return e.preventDefault();
15188
15189        // has non-editable
15190        if (this.selection.hasNonEditable())
15191        {
15192            e.preventDefault();
15193            return;
15194        }
15195
15196        // shift enter
15197        if (e.ctrlKey || e.shiftKey) {
15198            return this._insertBreak(e);
15199        }
15200
15201        // enter & exit
15202        if (this._isExit(e)) return;
15203
15204        // traverse
15205        this._traverse(e);
15206    },
15207    _disable: function(e)
15208    {
15209        e.preventDefault();
15210        var range = this.selection.getRange();
15211        if (range && !range.collapsed) range.deleteContents();
15212    },
15213    _insertBreak: function(e)
15214    {
15215        e.preventDefault();
15216
15217        var current = this.selection.getCurrent();
15218        var block = this.selection.getBlock();
15219        var data = this.inspector.parse(current);
15220
15221        if (data.isHeading() && this.caret.isStart(block)) {
15222            var $block = $R.dom(block);
15223            var $newBlock = $block.clone().html('');
15224            $block.before($newBlock);
15225            this.caret.setStart($block);
15226        }
15227        else if ((data.isComponent() && !data.isComponentEditable()) || data.isCode()) return;
15228        else if (data.isPre()) this.insertion.insertNewline();
15229        else this.insertion.insertBreakLine();
15230    },
15231    _isExit: function(e)
15232    {
15233        var $editor = this.editor.getElement();
15234        var block = this.selection.getBlock();
15235        var data = this.inspector.parse(block);
15236        var isEnd = this.caret.isEnd(block);
15237        var current = this.selection.getCurrent();
15238        var prev = current.previousSibling;
15239
15240        // blockquote
15241        if (data.isBlockquote())
15242        {
15243            var isParagraphExit = (isEnd && this._isExitableBlock(block, 'P'));
15244            var isBreaklineExit = (isEnd && this._isExitableDblBreak(prev));
15245
15246            if (isParagraphExit || isBreaklineExit)
15247            {
15248                return this._exitFromElement(e, ((isBreaklineExit) ? prev : block), data.getBlockquote());
15249            }
15250        }
15251        // pre
15252        else if (!data.isComponentType('code') && data.isPre())
15253        {
15254            if (isEnd)
15255            {
15256                var html = block.innerHTML;
15257                html = this.utils.removeInvisibleChars(html);
15258                if (html.match(/(\n\n\n)$/) !== null)
15259                {
15260                    $R.dom(prev.previousSibling.previousSibling).remove();
15261                    return this._exitFromElement(e, prev, block);
15262                }
15263            }
15264        }
15265        // dl
15266        else if (data.isDl())
15267        {
15268            if (isEnd && this._isExitableBlock(block, 'DT'))
15269            {
15270                return this._exitFromElement(e, block, data.getDl());
15271            }
15272        }
15273        // li
15274        else if (data.isList())
15275        {
15276            var list = $R.dom(current).parents('ul, ol', $editor).last();
15277
15278            isEnd = this.caret.isEnd(list);
15279            if (isEnd && this._isExitableBlock(block, 'LI'))
15280            {
15281                return this._exitFromElement(e, block, list);
15282            }
15283        }
15284        else if (data.isComponent() && data.isComponentActive() && !data.isFigcaption() && !data.isComponentEditable())
15285        {
15286            return this._exitFromElement(e, false, data.getComponent());
15287        }
15288    },
15289    _isExitableDblBreak: function(prev)
15290    {
15291        var next = (prev) ? prev.nextSibling : false;
15292        if (next)
15293        {
15294            var text = this.utils.removeInvisibleChars(next.textContent);
15295
15296            return (next.nodeType === 3 && text.trim() === '');
15297        }
15298    },
15299    _isExitableBlock: function(block, tag)
15300    {
15301        return (block && block.tagName === tag && this.utils.isEmptyHtml(block.innerHTML));
15302    },
15303    _exitFromElement: function(e, prev, el)
15304    {
15305        e.preventDefault();
15306        if (prev) $R.dom(prev).remove();
15307        this.utils.createMarkup(el);
15308
15309        return true;
15310    },
15311    _exitNextElement: function(e, node)
15312    {
15313        e.preventDefault();
15314
15315        if (node.nextSibling) this.caret.setStart(node.nextSibling);
15316        else this.utils.createMarkup(node);
15317
15318        return true;
15319    },
15320    _traverse: function(e)
15321    {
15322        var current = this.selection.getCurrent();
15323        var isText = this.selection.isText();
15324        var block = this.selection.getBlock();
15325        var data = this.inspector.parse(current);
15326        var blockTag = (block) ? block.tagName.toLowerCase() : false;
15327        var $variable = $R.dom(current).closest('[data-redactor-type=variable]');
15328        // variable parent
15329        if ($variable.length !== 0) {
15330            this.caret.setAfter($variable);
15331        }
15332
15333        // pre
15334        if (data.isPre())
15335        {
15336            e.preventDefault();
15337            return this.insertion.insertNewline();
15338        }
15339        // blockquote
15340        else if (data.isBlockquote())
15341        {
15342            block = this.selection.getBlock(current);
15343            if (block && block.tagName === 'BLOCKQUOTE')
15344            {
15345                e.preventDefault();
15346                return this.insertion.insertBreakLine();
15347            }
15348        }
15349        // figcaption
15350        else if (data.isFigcaption())
15351        {
15352            block = data.getFigcaption();
15353            var isEnd = this.caret.isEnd(block);
15354            var isEndEditor = this.caret.isEnd();
15355            if (isEnd || isEndEditor)
15356            {
15357                return this._exitNextElement(e, data.getComponent());
15358            }
15359            else
15360            {
15361                e.preventDefault();
15362                return;
15363            }
15364        }
15365        // dl
15366        else if (data.isDl())
15367        {
15368            e.preventDefault();
15369            return this._traverseDl(current);
15370        }
15371        // breakline
15372        else if (this.opts.breakline && blockTag === 'div') {
15373            setTimeout(this._replaceBlock.bind(this), 1);
15374            return;
15375        }
15376        // text
15377        else if (isText)
15378        {
15379            e.preventDefault();
15380            return this.insertion.insertBreakLine();
15381        }
15382        // list
15383        else if (data.isList()) {
15384            return;
15385        }
15386        // div / p
15387        else
15388        {
15389            var delay = (!this.detector.isDesktop()) ? 1 : 50;
15390            setTimeout(this._replaceBlock.bind(this), delay);
15391            return;
15392        }
15393
15394    },
15395    _traverseDl: function(current)
15396    {
15397        var block = this.selection.getBlock(current);
15398        var data = this.inspector.parse(block);
15399        var tag = data.getTag();
15400        var $el = $R.dom(block);
15401        var next = $el.get().nextSibling || false;
15402        var $next = $R.dom(next);
15403        var nextDd = (next && $next.is('dd'));
15404        var nextDt = (next && $next.is('dt'));
15405        var isEnd = this.caret.isEnd(block);
15406
15407        if (tag === 'dt' && !nextDd && isEnd)
15408        {
15409            var dd = document.createElement('dd');
15410            $el.after(dd);
15411
15412            this.caret.setStart(dd);
15413            return;
15414        }
15415        else if (tag === 'dd' && !nextDt && isEnd)
15416        {
15417            var dt = document.createElement('dt');
15418            $el.after(dt);
15419
15420            this.caret.setStart(dt);
15421            return;
15422        }
15423
15424        return this.insertion.insertBreakLine();
15425    },
15426    _replaceBlock: function()
15427    {
15428        var block = this.selection.getBlock();
15429        var $block = $R.dom(block);
15430
15431        if (this.opts.markup === 'p' && block && this._isNeedToReplaceBlock(block))
15432        {
15433            var markup = document.createElement(this.opts.markup);
15434            $block.replaceWith(markup);
15435            this.caret.setStart(markup);
15436        }
15437        else
15438        {
15439            if (block)
15440            {
15441                if (this.utils.isEmptyHtml(block.innerHTML))
15442                {
15443                    this._clearBlock($block, block);
15444                }
15445                else
15446                {
15447                    var first = this.utils.getFirstNode(block);
15448                    if (first && first.tagName === 'BR')
15449                    {
15450                        $R.dom(first).remove();
15451                        this.caret.setStart(block);
15452                    }
15453                }
15454            }
15455        }
15456
15457        // remove id
15458        $block.removeAttr('id');
15459
15460        if (block && this._isNeedToCleanBlockStyle(block) && this.opts.cleanOnEnter)
15461        {
15462            $block.removeAttr('class style');
15463        }
15464
15465        if (this.opts.breakline && block && block.tagName === 'DIV')
15466        {
15467            //$block.append(document.createElement('br'));
15468            //$block.attr('data-redactor-tag', 'br');
15469        }
15470    },
15471    _clearBlock: function($block, block)
15472    {
15473        if (block.tagName === 'DIV') {
15474            $block.find('br').remove();
15475        }
15476
15477        if (this.opts.cleanInlineOnEnter || block.innerHTML === '<br>')
15478        {
15479            $block.html('');
15480        }
15481
15482        this.caret.setStart(block);
15483    },
15484    _isNeedToReplaceBlock: function(block)
15485    {
15486        return (block.tagName === 'DIV' && this.utils.isEmptyHtml(block.innerHTML));
15487    },
15488    _isNeedToCleanBlockStyle: function(block)
15489    {
15490        return (block.tagName === 'P' && this.utils.isEmptyHtml(block.innerHTML));
15491    }
15492});
15493$R.add('class', 'input.paste', {
15494    init: function(app, e, dataTransfer, html, point)
15495    {
15496        this.app = app;
15497        this.opts = app.opts;
15498        this.editor = app.editor;
15499        this.cleaner = app.cleaner;
15500        this.container = app.container;
15501        this.inspector = app.inspector;
15502        this.insertion = app.insertion;
15503        this.selection = app.selection;
15504        this.autoparser = app.autoparser;
15505
15506        // local
15507        this.pasteHtml = html;
15508        this.pointInserted = point;
15509        this.dataTransfer = dataTransfer;
15510
15511        // init
15512        this._init(e);
15513    },
15514    // private
15515    _init: function(e)
15516    {
15517        var clipboard = this.dataTransfer || e.clipboardData;
15518        var current = this.selection.getCurrent();
15519        var dataCurrent = this.inspector.parse(current);
15520
15521        this.dropPasted = this.dataTransfer;
15522        //this.isRawCode = (this.dropPasted) ? (dataCurrent.isPre() || dataCurrent.isCode()) : this._isPlainText(clipboard);
15523        this.isRawCode = (dataCurrent.isPre() || dataCurrent.isCode());
15524
15525        this.editor.enablePasting();
15526        this.editor.saveScroll();
15527
15528        if (!this.dropPasted)
15529        {
15530            this.selection.saveMarkers();
15531        }
15532
15533        if (this.isRawCode || !clipboard)
15534        {
15535
15536            var text;
15537            if (!this.isRawCode && !clipboard && window.clipboardData)
15538            {
15539                text = window.clipboardData.getData("text");
15540            }
15541            else
15542            {
15543                text = clipboard.getData("text/plain");
15544            }
15545
15546            e.preventDefault();
15547            this._insert(e, text);
15548            return;
15549        }
15550        else if (this.pasteHtml)
15551        {
15552            e.preventDefault();
15553            this._insert(e, this.pasteHtml);
15554        }
15555        else
15556        {
15557            // html / text
15558            var url = clipboard.getData('URL');
15559            var html = (this._isPlainText(clipboard)) ? this.cleaner.encodeEntities(clipboard.getData("text/plain")) : clipboard.getData("text/html");
15560
15561            // safari anchor links
15562            html = (!url || url === '') ? html : url;
15563
15564            // file
15565            if (clipboard.files !== null && clipboard.files.length > 0 && html === '')
15566            {
15567                var files = [];
15568                for (var i = 0; i < clipboard.files.length; i++)
15569                {
15570                    var file = clipboard.files[i] || clipboard.items[i].getAsFile();
15571                    if (file) files.push(file);
15572                }
15573
15574                if (files.length > 0)
15575                {
15576                    e.preventDefault();
15577                    this._insertFiles(e, files);
15578                    return;
15579                }
15580            }
15581
15582
15583            e.preventDefault();
15584            this._insert(e, html);
15585        }
15586    },
15587    _isPlainText: function(clipboard)
15588    {
15589        var text = clipboard.getData("text/plain");
15590        var html = clipboard.getData("text/html");
15591
15592        if (text && html)
15593        {
15594            var element = document.createElement("div");
15595            element.innerHTML = html;
15596
15597            if (element.textContent === text)
15598            {
15599                return !element.querySelector(":not(meta)");
15600            }
15601        }
15602        else
15603        {
15604            return (text !== null);
15605        }
15606    },
15607    _restoreSelection: function()
15608    {
15609        this.editor.restoreScroll();
15610        this.editor.disablePasting();
15611        if (!this.dropPasted)
15612        {
15613            this.selection.restoreMarkers();
15614        }
15615    },
15616    _insert: function(e, html)
15617    {
15618        // pasteBefore callback
15619        var returned = this.app.broadcast('pasteBefore', html);
15620        html = (returned === undefined) ? html : returned;
15621
15622        // clean
15623        html = html.trim();
15624        html = (this.isRawCode) ? html : this.cleaner.paste(html);
15625        html = html.trim();
15626        html = (this.isRawCode) ? this.cleaner.encodePhpCode(html) : html;
15627
15628        // paste callback
15629        returned = this.app.broadcast('pasting', html);
15630        html = (returned === undefined) ? html : returned;
15631
15632        this._restoreSelection();
15633
15634        // stop input
15635        if (!this.opts.input) return;
15636
15637        this.app.broadcast('state', false);
15638
15639        // insert
15640        var nodes = [];
15641        if (this.isRawCode) {
15642            html = html.replace('&lt;?php', '<?php');
15643
15644            var textNode = document.createTextNode(html);
15645            nodes = this.insertion.insertNode(textNode, 'after');
15646
15647            this.app.broadcast('pasted', nodes);
15648        }
15649        else {
15650
15651            // autoparse
15652            if (this.opts.autoparse && this.opts.autoparsePaste)
15653            {
15654                html = this.autoparser.parse(html);
15655            }
15656
15657            nodes = (this.dropPasted) ? this.insertion.insertToPoint(e, html, this.pointInserted) : this.insertion.insertHtml(html);
15658
15659            // pasted callback
15660            this.app.broadcast('pasted', nodes);
15661            this.app.broadcast('autoparseobserve');
15662        }
15663
15664    },
15665    _insertFiles: function(e, files)
15666    {
15667        this._restoreSelection();
15668
15669        // drop or clipboard
15670        var isImage = (this.opts.imageTypes.indexOf(files[0].type) !== -1);
15671        var isClipboard = (typeof this.dropPasted === 'undefined');
15672
15673        if (isImage) this.app.broadcast('dropimage', e, files, isClipboard);
15674        else this.app.broadcast('dropfile', e, files, isClipboard);
15675    }
15676});
15677$R.add('class', 'input.shortcode', {
15678    init: function(app, e, key)
15679    {
15680        this.app = app;
15681        this.opts = app.opts;
15682        this.utils = app.utils;
15683        this.marker = app.marker;
15684        this.keycodes = app.keycodes;
15685        this.selection = app.selection;
15686
15687        // local
15688        this.worked = false;
15689
15690        // init
15691        if (key === this.keycodes.SPACE) this._init();
15692    },
15693    // public
15694    is: function()
15695    {
15696        return this.worked;
15697    },
15698    // private
15699    _init: function()
15700    {
15701        var current = this.selection.getCurrent();
15702        if (current && current.nodeType === 3)
15703        {
15704            var text = this.utils.removeInvisibleChars(current.textContent);
15705            var shortcodes = this.opts.shortcodes;
15706            for (var name in shortcodes)
15707            {
15708                var re = new RegExp('^' + this.utils.escapeRegExp(name));
15709                var match = text.match(re);
15710                if (match !== null)
15711                {
15712                    if (typeof shortcodes[name].format !== 'undefined')
15713                    {
15714                        return this._format(shortcodes[name].format, current, re);
15715                    }
15716                }
15717            }
15718        }
15719    },
15720    _format: function(tag, current, re)
15721    {
15722        var marker = this.marker.insert('start');
15723        current = marker.previousSibling;
15724
15725        var text = current.textContent;
15726        text = this.utils.trimSpaces(text);
15727        text = text.replace(re, '');
15728        current.textContent = text;
15729
15730        var api = (tag === 'ul' || tag === 'ol') ? 'module.list.toggle' : 'module.block.format';
15731
15732        this.app.api(api, tag);
15733        this.selection.restoreMarkers();
15734
15735        this.worked = true;
15736    }
15737});
15738$R.add('class', 'input.shortcut', {
15739    init: function(app, e)
15740    {
15741        this.app = app;
15742        this.opts = app.opts;
15743
15744        // local
15745        this.worked = false;
15746
15747        // based on https://github.com/jeresig/jquery.hotkeys
15748        this.hotkeys = {
15749            8: "backspace", 9: "tab", 10: "return", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause",
15750            20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home",
15751            37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", 59: ";", 61: "=",
15752            96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7",
15753            104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/",
15754            112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8",
15755            120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 173: "-", 186: ";", 187: "=",
15756            188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", 221: "]", 222: "'"
15757        };
15758
15759        this.hotkeysShiftNums = {
15760            "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&",
15761            "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<",
15762            ".": ">",  "/": "?",  "\\": "|"
15763        };
15764
15765        // init
15766        this._init(e);
15767    },
15768    // public
15769    is: function()
15770    {
15771        return this.worked;
15772    },
15773    // private
15774    _init: function(e)
15775    {
15776        // disable browser's hot keys for bold and italic if shortcuts off
15777        if (this.opts.shortcuts === false)
15778        {
15779            if ((e.ctrlKey || e.metaKey) && (e.which === 66 || e.which === 73)) e.preventDefault();
15780            return;
15781        }
15782
15783        // build
15784        for (var key in this.opts.shortcuts)
15785        {
15786            this._build(e, key, this.opts.shortcuts[key]);
15787        }
15788    },
15789    _build: function(e, str, command)
15790    {
15791        var keys = str.split(',');
15792        var len = keys.length;
15793        for (var i = 0; i < len; i++)
15794        {
15795            if (typeof keys[i] === 'string')
15796            {
15797                this._handler(e, keys[i].trim(), command);
15798            }
15799        }
15800    },
15801    _handler: function(e, keys, command)
15802    {
15803        keys = keys.toLowerCase().split(" ");
15804
15805        var special = this.hotkeys[e.keyCode];
15806        var character = String.fromCharCode(e.which).toLowerCase();
15807        var modif = "", possible = {};
15808        var cmdKeys = ["meta", "ctrl", "alt", "shift"];
15809
15810        for (var i = 0; i < cmdKeys.length; i++)
15811        {
15812            var specialKey = cmdKeys[i];
15813            if (e[specialKey + 'Key'] && special !== specialKey)
15814            {
15815                modif += specialKey + '+';
15816            }
15817        }
15818
15819        if (special) possible[modif + special] = true;
15820        if (character)
15821        {
15822            possible[modif + character] = true;
15823            possible[modif + this.hotkeysShiftNums[character]] = true;
15824
15825            // "$" can be triggered as "Shift+4" or "Shift+$" or just "$"
15826            if (modif === "shift+")
15827            {
15828                possible[this.hotkeysShiftNums[character]] = true;
15829            }
15830        }
15831
15832        var len = keys.length;
15833        for (var i = 0; i < len; i++)
15834        {
15835            if (possible[keys[i]])
15836            {
15837
15838                e.preventDefault();
15839                this.worked = true;
15840
15841                if (command.message)
15842                {
15843                    this.app.broadcast(command.message, command.args);
15844                    this.app.broadcast('buffer.trigger');
15845                }
15846                else if (command.api)
15847                {
15848                    this.app.api(command.api, command.args);
15849                    this.app.broadcast('buffer.trigger');
15850                }
15851
15852                return;
15853            }
15854        }
15855    }
15856});
15857$R.add('class', 'input.space', {
15858    init: function(app, e, key, lastShiftKey)
15859    {
15860        this.app = app;
15861        this.keycodes = app.keycodes;
15862        this.insertion = app.insertion;
15863        this.selection = app.selection;
15864
15865        // local
15866        this.key = key;
15867        this.lastShiftKey = lastShiftKey;
15868
15869        // init
15870        this._init(e);
15871    },
15872    // private
15873    _init: function(e)
15874    {
15875        // has non-editable
15876        if (this.selection.hasNonEditable())
15877        {
15878            e.preventDefault();
15879            return;
15880        }
15881
15882        // shift/ctrl + space
15883        if (!this.lastShiftKey && this.key === this.keycodes.SPACE && (e.ctrlKey || e.shiftKey) && !e.metaKey)
15884        {
15885            e.preventDefault();
15886            this.insertion.insertChar('&nbsp;');
15887            return;
15888        }
15889    }
15890});
15891$R.add('class', 'input.tab', {
15892    init: function(app, e)
15893    {
15894        this.app = app;
15895        this.opts = app.opts;
15896        this.inspector = app.inspector;
15897        this.insertion = app.insertion;
15898        this.selection = app.selection;
15899
15900        // init
15901        this._init(e);
15902    },
15903    // private
15904    _init: function(e)
15905    {
15906        // turn off tab
15907        if (!this.opts.tabKey) return;
15908
15909        // callback
15910        var stop = this.app.broadcast('tab', e);
15911        if (stop === false) return e.preventDefault();
15912
15913        // traverse
15914        this._traverse(e);
15915    },
15916    _traverse: function(e)
15917    {
15918        var current = this.selection.getCurrent();
15919        var data = this.inspector.parse(current);
15920
15921        // hard tab
15922        if (!data.isComponent() && e.shiftKey)
15923        {
15924            return this._insertHardTab(e, 4);
15925        }
15926
15927        // list
15928        if (data.isList())
15929        {
15930            e.preventDefault();
15931            return this.app.api('module.list.indent');
15932        }
15933        // pre
15934        if (data.isPre() || (data.isComponentType('code') && !data.isFigcaption()))
15935        {
15936            return this._tabCode(e);
15937        }
15938
15939        // tab as spaces
15940        if (this.opts.tabAsSpaces !== false)
15941        {
15942            return this._insertHardTab(e, this.opts.tabAsSpaces);
15943        }
15944    },
15945    _insertHardTab: function(e, num)
15946    {
15947        e.preventDefault();
15948        var node = document.createTextNode(Array(num + 1).join('\u00a0'));
15949        return this.insertion.insertNode(node, 'end');
15950    },
15951    _tabCode: function(e)
15952    {
15953        e.preventDefault();
15954
15955        var node = (this.opts.preSpaces) ? document.createTextNode(Array(this.opts.preSpaces + 1).join('\u00a0')) : document.createTextNode('\t');
15956
15957        return this.insertion.insertNode(node, 'end');
15958    }
15959});
15960$R.add('module', 'upload', {
15961    init: function(app)
15962    {
15963        this.app = app;
15964        this.opts = app.opts;
15965        this.lang = app.lang;
15966        this.utils = app.utils;
15967        this.editor = app.editor;
15968        this.progress = app.progress;
15969
15970        // local
15971        this.defaults = {
15972            event: false,
15973            element: false,
15974            name: false,
15975            files: false,
15976            url: false,
15977            data: false,
15978            paramName: false
15979        };
15980    },
15981    // public
15982    build: function(options)
15983    {
15984        this.p = $R.extend(this.defaults, options);
15985        this.$el = $R.dom(this.p.element);
15986
15987        if (this.$el.get().tagName === 'INPUT') this._buildInput();
15988        else                                    this._buildBox();
15989    },
15990    send: function(options)
15991    {
15992        this.p = $R.extend(this.defaults, options);
15993        this.$uploadbox = this.editor.getElement();
15994        this._send(this.p.event, this.p.files);
15995    },
15996    complete: function(response, e)
15997    {
15998        this._complete(response, e);
15999    },
16000
16001    // private
16002    _buildInput: function()
16003    {
16004        this.box = false;
16005        this.prefix = '';
16006
16007        this.$uploadbox = $R.dom('<div class="upload-redactor-box" />');
16008
16009        this.$el.hide();
16010        this.$el.after(this.$uploadbox);
16011
16012        if (this.opts.multipleUpload) this.$el.attr('multiple', 'multiple');
16013        else this.$el.removeAttr('multiple');
16014
16015        if (this.p.name !== 'file')
16016        {
16017            this.$el.attr('accept', 'image/*');
16018        }
16019
16020        this._buildPlaceholder();
16021        this._buildEvents();
16022    },
16023    _buildBox: function()
16024    {
16025        this.box = true;
16026        this.prefix = 'box-';
16027
16028        this.$uploadbox = this.$el;
16029        this.$uploadbox.attr('ondragstart', 'return false;');
16030
16031        // events
16032        this.$uploadbox.on('drop.redactor.upload', this._onDropBox.bind(this));
16033        this.$uploadbox.on('dragover.redactor.upload', this._onDragOver.bind(this));
16034        this.$uploadbox.on('dragleave.redactor.upload', this._onDragLeave.bind(this));
16035    },
16036    _buildPlaceholder: function()
16037    {
16038        this.$placeholder = $R.dom('<div class="upload-redactor-placeholder" />');
16039        this.$placeholder.html(this.lang.get('upload-label'));
16040        this.$uploadbox.append(this.$placeholder);
16041    },
16042    _buildEvents: function()
16043    {
16044        this.$el.on('change.redactor.upload', this._onChange.bind(this));
16045        this.$uploadbox.on('click.redactor.upload', this._onClick.bind(this));
16046        this.$uploadbox.on('drop.redactor.upload', this._onDrop.bind(this));
16047        this.$uploadbox.on('dragover.redactor.upload', this._onDragOver.bind(this));
16048        this.$uploadbox.on('dragleave.redactor.upload', this._onDragLeave.bind(this));
16049    },
16050    _onClick: function(e)
16051    {
16052        e.preventDefault();
16053        this.$el.click();
16054    },
16055    _onChange: function(e)
16056    {
16057        this._send(e, this.$el.get().files);
16058    },
16059    _onDrop: function(e)
16060    {
16061        e.preventDefault();
16062
16063        this._clear();
16064        this._setStatusDrop();
16065        this._send(e);
16066    },
16067    _onDragOver: function(e)
16068    {
16069        e.preventDefault();
16070        this._setStatusHover();
16071
16072        return false;
16073    },
16074    _onDragLeave: function(e)
16075    {
16076        e.preventDefault();
16077        this._removeStatusHover();
16078
16079        return false;
16080    },
16081    _onDropBox: function(e)
16082    {
16083        e.preventDefault();
16084
16085        this._clear();
16086        this._setStatusDrop();
16087        this._send(e);
16088    },
16089    _removeStatusHover: function()
16090    {
16091        this.$uploadbox.removeClass('upload-redactor-' + this.prefix + 'hover');
16092    },
16093    _setStatusDrop: function()
16094    {
16095        this.$uploadbox.addClass('upload-redactor-' + this.prefix + 'drop');
16096    },
16097    _setStatusHover: function()
16098    {
16099        this.$uploadbox.addClass('upload-redactor-' + this.prefix + 'hover');
16100    },
16101    _setStatusError: function()
16102    {
16103        this.$uploadbox.addClass('upload-redactor-' + this.prefix + 'error');
16104    },
16105    _setStatusSuccess: function()
16106    {
16107        this.$uploadbox.addClass('upload-redactor-' + this.prefix + 'success');
16108    },
16109    _clear: function()
16110    {
16111        var classes = ['drop', 'hover', 'error', 'success'];
16112        for (var i = 0; i < classes.length; i++)
16113        {
16114            this.$uploadbox.removeClass('upload-redactor-' + this.prefix + classes[i]);
16115        }
16116
16117        this.$uploadbox.removeAttr('ondragstart');
16118    },
16119    _send: function(e, files)
16120    {
16121        e = e.originalEvent || e;
16122
16123        files = (files) ? files : e.dataTransfer.files;
16124
16125        var data = new FormData();
16126        var name = this._getUploadParam();
16127
16128        data = this._buildData(name, files, data);
16129        data = this.utils.extendData(data, this.p.data);
16130
16131        var stop = this.app.broadcast('upload.start', e, data, files);
16132        if (stop !== false)
16133        {
16134            this._sendData(data, files, e);
16135        }
16136    },
16137    _sendData: function(data, files, e)
16138    {
16139        this.progress.show();
16140        if (typeof this.p.url === 'function')
16141        {
16142            var res = this.p.url(data, files, e, this);
16143            if (!(res instanceof Promise))
16144            {
16145                this._complete(res, e);
16146            }
16147        }
16148        else
16149        {
16150            $R.ajax.post({
16151                url: this.p.url,
16152                data: data,
16153                before: function(xhr)
16154                {
16155                    return this.app.broadcast('upload.beforeSend', xhr);
16156
16157                }.bind(this),
16158                success: function(response)
16159                {
16160                    this._complete(response, e);
16161                }.bind(this),
16162                error: function(response, xhr, status)
16163                {
16164                    this._complete(response, e, status);
16165                }.bind(this)
16166            });
16167        }
16168    },
16169    _getUploadParam: function()
16170    {
16171        return (this.p.paramName) ? this.p.paramName : 'file';
16172    },
16173    _buildData: function(name, files, data)
16174    {
16175        if (files.length === 1)
16176        {
16177            data.append(name + '[]', files[0]);
16178        }
16179        else if (files.length > 1 && this.opts.multipleUpload !== false)
16180        {
16181            for (var i = 0; i < files.length; i++)
16182            {
16183                data.append(name + '[]', files[i]);
16184            }
16185        }
16186
16187        return data;
16188    },
16189    _complete: function(response, e, status)
16190    {
16191        this._clear();
16192        this.progress.hide();
16193
16194        if (response && response.error)
16195        {
16196            this._setStatusError();
16197
16198            this.app.broadcast('upload.' + this.p.name + '.error', response, e, status);
16199            this.app.broadcast('upload.error', response, status);
16200        }
16201        else
16202        {
16203            this._setStatusSuccess();
16204
16205            this.app.broadcast('upload.' + this.p.name + '.complete', response, e);
16206            this.app.broadcast('upload.complete', response);
16207
16208            setTimeout(this._clear.bind(this), 500);
16209        }
16210    }
16211});
16212$R.add('class', 'code.component', {
16213    mixins: ['dom', 'component'],
16214    init: function(app, el)
16215    {
16216        this.app = app;
16217
16218        // init
16219        return (el && el.cmnt !== undefined) ? el : this._init(el);
16220    },
16221
16222    // private
16223   _init: function(el)
16224    {
16225        var $pre;
16226        if (typeof el !== 'undefined')
16227        {
16228            var $node = $R.dom(el);
16229            var $wrapper = $node.closest('figure');
16230            if ($wrapper.length !== 0)
16231            {
16232                this.parse($wrapper);
16233            }
16234            else
16235            {
16236                this.parse('<figure>');
16237                this.append(el);
16238            }
16239
16240            $pre = this.find('pre code, pre').last();
16241        }
16242        else
16243        {
16244            $pre = $R.dom('<pre>');
16245
16246            this.parse('<figure>');
16247            this.append($pre);
16248        }
16249
16250        this._initElement($pre);
16251        this._initWrapper();
16252    },
16253    _initElement: function($pre)
16254    {
16255        $pre.attr({
16256            'tabindex': '-1',
16257            'contenteditable': true
16258        });
16259    },
16260    _initWrapper: function()
16261    {
16262        this.addClass('redactor-component');
16263        this.attr({
16264            'data-redactor-type': 'code',
16265            'tabindex': '-1',
16266            'contenteditable': false
16267        });
16268    }
16269});
16270$R.add('module', 'form', {
16271    init: function(app)
16272    {
16273        this.app = app;
16274        this.lang = app.lang;
16275        this.component = app.component;
16276        this.inspector = app.inspector;
16277    },
16278    // messages
16279    onform: {
16280        remove: function(node)
16281        {
16282            this._remove(node);
16283        }
16284    },
16285    oncontextbar: function(e, contextbar)
16286    {
16287        var data = this.inspector.parse(e.target);
16288        if (data.isComponentType('form'))
16289        {
16290            var node = data.getComponent();
16291            var buttons = {
16292                "remove": {
16293                    title: this.lang.get('delete'),
16294                    api: 'module.form.remove',
16295                    args: node
16296                }
16297            };
16298
16299            contextbar.set(e, node, buttons, 'top');
16300        }
16301
16302    },
16303
16304    // private
16305    _remove: function(node)
16306    {
16307        this.component.remove(node);
16308    }
16309});
16310$R.add('class', 'form.component', {
16311    mixins: ['dom', 'component'],
16312    init: function(app, el)
16313    {
16314        this.app = app;
16315        this.utils = app.utils;
16316
16317        // init
16318        return (el && el.cmnt !== undefined) ? el : this._init(el);
16319    },
16320    // private
16321    _init: function(el)
16322    {
16323        if (typeof el !== 'undefined')
16324        {
16325            var $node = $R.dom(el);
16326            var $wrapper = $node.closest('form');
16327            if ($wrapper.length !== 0)
16328            {
16329                var $figure = this.utils.replaceToTag(el, 'figure');
16330                this.parse($figure);
16331            }
16332            else
16333            {
16334                this.parse('<figure>');
16335                this.append(el);
16336            }
16337        }
16338        else
16339        {
16340            this.parse('<figure>');
16341        }
16342
16343        this._initWrapper();
16344    },
16345    _initWrapper: function()
16346    {
16347        this.addClass('redactor-component');
16348        this.attr({
16349            'data-redactor-type': 'form',
16350            'tabindex': '-1',
16351            'contenteditable': false
16352        });
16353    }
16354});
16355$R.add('module', 'image', {
16356    modals: {
16357        'image':
16358            '<div class="redactor-modal-tab redactor-modal-tab-upload" data-title="## upload ##"><form action=""> \
16359                <input type="file" name="file"> \
16360            </form></div>',
16361        'imageedit':
16362            '<div class="redactor-modal-group"> \
16363                <div id="redactor-modal-image-preview" class="redactor-modal-side"></div> \
16364                <form action="" class="redactor-modal-area"> \
16365                    <div class="form-item"> \
16366                        <label for="modal-image-title"> ## title ##</label> \
16367                        <input type="text" id="modal-image-title" name="title" /> \
16368                    </div> \
16369                    <div class="form-item form-item-caption"> \
16370                        <label for="modal-image-caption">## caption ##</label> \
16371                        <input type="text" id="modal-image-caption" name="caption" aria-label="## caption ##" /> \
16372                    </div> \
16373                    <div class="form-item form-item-align"> \
16374                        <label>## image-position ##</label> \
16375                        <select name="align" aria-label="## image-position ##"> \
16376                            <option value="none">## none ##</option> \
16377                            <option value="left">## left ##</option> \
16378                            <option value="center">## center ##</option> \
16379                            <option value="right">## right ##</option> \
16380                        </select> \
16381                    </div> \
16382                    <div class="form-item form-item-link"> \
16383                        <label for="modal-image-url">## link ##</label> \
16384                        <input type="text" id="modal-image-url" name="url" aria-label="## link ##" /> \
16385                    </div> \
16386                    <div class="form-item form-item-link"> \
16387                        <label class="checkbox"><input type="checkbox" name="target" aria-label="## link-in-new-tab ##"> ## link-in-new-tab ##</label> \
16388                    </div> \
16389                </form> \
16390            </div>'
16391    },
16392    init: function(app)
16393    {
16394        this.app = app;
16395        this.opts = app.opts;
16396        this.lang = app.lang;
16397        this.caret = app.caret;
16398        this.utils = app.utils;
16399        this.editor = app.editor;
16400        this.storage = app.storage;
16401        this.component = app.component;
16402        this.inspector = app.inspector;
16403        this.insertion = app.insertion;
16404        this.selection = app.selection;
16405
16406        // local
16407        this.justResized = false;
16408    },
16409    // messages
16410    oninsert: function()
16411    {
16412        this._observeImages();
16413    },
16414    onstarted: function()
16415    {
16416        // storage observe
16417        this.storage.observeImages();
16418
16419        // resize
16420        if (this.opts.imageResizable)
16421        {
16422            this.resizer = $R.create('image.resize', this.app);
16423        }
16424
16425        // observe
16426        this._observeImages();
16427    },
16428    ondropimage: function(e, files, clipboard)
16429    {
16430        if (!this.opts.imageUpload) return;
16431
16432        var options = {
16433            url: this.opts.imageUpload,
16434            event: (clipboard) ? false : e,
16435            files: files,
16436            name: 'imagedrop',
16437            data: this.opts.imageData,
16438            paramName: this.opts.imageUploadParam
16439        };
16440
16441        this.app.api('module.upload.send', options);
16442    },
16443    onstop: function()
16444    {
16445        if (this.resizer) this.resizer.stop();
16446    },
16447    onbottomclick: function()
16448    {
16449        this.insertion.insertToEnd(this.editor.getLastNode(), 'image');
16450    },
16451    onimageresizer: {
16452        stop: function()
16453        {
16454            if (this.resizer) this.resizer.hide();
16455        }
16456    },
16457    onsource: {
16458        open: function()
16459        {
16460            if (this.resizer) this.resizer.hide();
16461        },
16462        closed: function()
16463        {
16464            this._observeImages();
16465            if (this.resizer) this.resizer.rebuild();
16466        }
16467    },
16468    onupload: {
16469        complete: function()
16470        {
16471            this._observeImages();
16472        },
16473        image: {
16474            complete: function(response)
16475            {
16476                this._insert(response);
16477                this.app.broadcast('state', false);
16478            },
16479            error: function(response)
16480            {
16481                this._uploadError(response);
16482            }
16483        },
16484        imageedit: {
16485            complete: function(response)
16486            {
16487                this._change(response);
16488            },
16489            error: function(response)
16490            {
16491                this._uploadError(response);
16492            }
16493        },
16494        imagedrop: {
16495            complete: function(response, e)
16496            {
16497                this._insert(response, e);
16498            },
16499            error: function(response)
16500            {
16501                this._uploadError(response);
16502            }
16503        },
16504        imagereplace: {
16505            complete: function(response)
16506            {
16507                this._change(response, false);
16508            },
16509            error: function(response)
16510            {
16511                this._uploadError(response);
16512            }
16513        }
16514    },
16515    onmodal: {
16516        image: {
16517            open: function($modal, $form)
16518            {
16519                this._setUpload($modal, $form);
16520            }
16521        },
16522        imageedit: {
16523            open: function($modal, $form)
16524            {
16525                this._setFormData($modal, $form);
16526            },
16527            opened: function($modal, $form)
16528            {
16529                this._setFormFocus($form);
16530            },
16531            remove: function()
16532            {
16533                this._remove(this.$image);
16534            },
16535            save: function($modal, $form)
16536            {
16537                this._save($modal, $form);
16538            }
16539        }
16540    },
16541    onimage: {
16542        observe: function()
16543        {
16544            this._observeImages();
16545        },
16546        resized: function()
16547        {
16548            this.justResized = true;
16549        }
16550    },
16551    oncontextbar: function(e, contextbar)
16552    {
16553        if (this.justResized)
16554        {
16555            this.justResized = false;
16556            return;
16557        }
16558
16559        var current = this.selection.getCurrent();
16560        var data = this.inspector.parse(current);
16561        var $img = $R.dom(current).closest('img');
16562
16563        if ((!data.isFigcaption() && data.isComponentType('image')) || $img.length !== 0)
16564        {
16565            var node = ($img.length !== 0) ? $img.get() : data.getComponent();
16566            var buttons = {
16567                "edit": {
16568                    title: this.lang.get('edit'),
16569                    api: 'module.image.open'
16570                },
16571                "remove": {
16572                    title: this.lang.get('delete'),
16573                    api: 'module.image.remove',
16574                    args: node
16575                }
16576            };
16577
16578            contextbar.set(e, node, buttons);
16579        }
16580    },
16581
16582    // public
16583    open: function()
16584    {
16585        this.$image = this._getCurrent();
16586        this.app.api('module.modal.build', this._getModalData());
16587    },
16588    insert: function(data)
16589    {
16590        this._insert(data);
16591    },
16592    remove: function(node)
16593    {
16594        this._remove(node);
16595    },
16596
16597    // private
16598    _getModalData: function()
16599    {
16600        var modalData;
16601        if (this._isImage() && this.opts.imageEditable)
16602        {
16603            modalData = {
16604                name: 'imageedit',
16605                width: '800px',
16606                title: this.lang.get('edit'),
16607                handle: 'save',
16608                commands: {
16609                    save: { title: this.lang.get('save') },
16610                    remove: { title: this.lang.get('delete'), type: 'danger' },
16611                    cancel: { title: this.lang.get('cancel') }
16612                }
16613            };
16614        }
16615        else
16616        {
16617            modalData = {
16618                name: 'image',
16619                title: this.lang.get('image')
16620            };
16621        }
16622
16623        return modalData;
16624    },
16625    _isImage: function()
16626    {
16627        return this.$image;
16628    },
16629    _getCurrent: function()
16630    {
16631        var current = this.selection.getCurrent();
16632        var data = this.inspector.parse(current);
16633        var $img = $R.dom(current).closest('img');
16634
16635        if ($img.length !== 0) {
16636            return this.component.create('image', $img);
16637        }
16638        else {
16639            return (data.isComponentType('image') && data.isComponentActive()) ? this.component.create('image', data.getComponent()) : false;
16640        }
16641    },
16642    _insert: function(response, e)
16643    {
16644        this.app.api('module.modal.close');
16645
16646        if (Array.isArray(response))
16647        {
16648            var obj = {};
16649            for (var i = 0; i < response.length; i++)
16650            {
16651                obj = $R.extend(obj, response[i]);
16652            }
16653
16654            response = obj;
16655        }
16656        else if (typeof response === 'string')
16657        {
16658            response = { "file": { url: response }};
16659        }
16660
16661        if (typeof response === 'object')
16662        {
16663
16664            var multiple = 0;
16665            for (var key in response)
16666            {
16667                if (typeof response[key] === 'object') multiple++;
16668            }
16669
16670            if (multiple > 1)
16671            {
16672                this._insertMultiple(response, e);
16673            }
16674            else
16675            {
16676                this._insertSingle(response, e);
16677            }
16678        }
16679    },
16680    _insertSingle: function(response, e)
16681    {
16682        for (var key in response)
16683        {
16684            if (typeof response[key] === 'object')
16685            {
16686                var $img = this._createImageAndStore(response[key]);
16687                var inserted = (e) ? this.insertion.insertToPoint(e, $img, false, false) : this.insertion.insertHtml($img, false);
16688
16689                this._removeSpaceBeforeFigure(inserted[0]);
16690
16691                // set is active
16692                this.component.setActive(inserted[0]);
16693                this.app.broadcast('image.uploaded', inserted[0], response);
16694            }
16695        }
16696    },
16697    _insertMultiple: function(response, e)
16698    {
16699        var z = 0;
16700        var inserted = [];
16701        var last;
16702        for (var key in response)
16703        {
16704            if (typeof response[key] === 'object')
16705            {
16706                z++;
16707
16708                var $img = this._createImageAndStore(response[key]);
16709
16710                if (z === 1)
16711                {
16712                    inserted = (e) ? this.insertion.insertToPoint(e, $img, false, false) : this.insertion.insertHtml($img, false);
16713                }
16714                else
16715                {
16716                    var $inserted = $R.dom(inserted[0]);
16717                    $inserted.after($img);
16718                    inserted = [$img.get()];
16719
16720                    this.app.broadcast('image.inserted', $img);
16721                }
16722
16723                last = inserted[0];
16724
16725                this._removeSpaceBeforeFigure(inserted[0]);
16726                this.app.broadcast('image.uploaded', inserted[0], response);
16727            }
16728        }
16729
16730        // set last is active
16731        this.component.setActive(last);
16732    },
16733    _createImageAndStore: function(item)
16734    {
16735        var $img = this.component.create('image');
16736
16737        $img.addClass('redactor-uploaded-figure');
16738        $img.setData({
16739            src: item.url,
16740            id: (item.id) ? item.id : this.utils.getRandomId()
16741        });
16742
16743        // add to storage
16744        this.storage.add('image', $img.getElement());
16745
16746        return $img;
16747    },
16748    _removeSpaceBeforeFigure: function(img)
16749    {
16750        if (!img) return;
16751
16752        var prev = img.previousSibling;
16753        var next = img.nextSibling;
16754        var $prev = $R.dom(prev);
16755        var $next = $R.dom(next);
16756
16757        if (this.opts.breakline) {
16758            if (next && $next.attr('data-redactor-tag') === 'br') {
16759                $next.find('br').first().remove();
16760            }
16761            if (prev && $prev.attr('data-redactor-tag') === 'br') {
16762                $prev.find('br').last().remove();
16763            }
16764        }
16765
16766        if (prev)
16767        {
16768            this._removeInvisibleSpace(prev);
16769            this._removeInvisibleSpace(prev.previousSibling);
16770        }
16771    },
16772    _removeInvisibleSpace: function(el)
16773    {
16774        if (el && el.nodeType === 3 && this.utils.searchInvisibleChars(el.textContent) !== -1)
16775        {
16776            el.parentNode.removeChild(el);
16777        }
16778    },
16779    _save: function($modal, $form)
16780    {
16781        var data = $form.getData();
16782        var imageData = {
16783            title: data.title
16784        };
16785
16786        if (this.opts.imageLink) imageData.link = { url: data.url, target: data.target };
16787        if (this.opts.imageCaption) imageData.caption = data.caption;
16788        if (this.opts.imagePosition) imageData.align = data.align;
16789
16790        this.$image.setData(imageData);
16791        if (this.resizer) this.resizer.rebuild();
16792
16793        this.app.broadcast('image.changed', this.$image);
16794        this.app.api('module.modal.close');
16795    },
16796    _change: function(response, modal)
16797    {
16798        if (typeof response === 'string')
16799        {
16800            response = { "file": { url: response }};
16801        }
16802
16803        if (typeof response === 'object')
16804        {
16805            var $img;
16806            for (var key in response)
16807            {
16808                if (typeof response[key] === 'object')
16809                {
16810                    $img = $R.dom('<img>');
16811                    $img.attr('src', response[key].url);
16812
16813                    this.$image.changeImage(response[key]);
16814
16815                    this.app.broadcast('image.changed', this.$image, response);
16816                    this.app.broadcast('image.uploaded', this.$image, response);
16817
16818                    this.app.broadcast('hardsync');
16819
16820                    break;
16821                }
16822            }
16823
16824            if (modal !== false)
16825            {
16826                $img.on('load', function() { this.$previewBox.html($img); }.bind(this));
16827            }
16828        }
16829    },
16830    _uploadError: function(response)
16831    {
16832        this.app.broadcast('image.uploadError', response);
16833    },
16834    _remove: function(node)
16835    {
16836        this.app.api('module.modal.close');
16837        this.component.remove(node);
16838    },
16839    _observeImages: function()
16840    {
16841        var $editor = this.editor.getElement();
16842        var self = this;
16843        $editor.find('img').each(function(node)
16844        {
16845            var $node = $R.dom(node);
16846
16847            $node.off('.drop-to-replace');
16848            $node.on('dragover.drop-to-replace dragenter.drop-to-replace', function(e)
16849            {
16850                e.preventDefault();
16851                return;
16852            });
16853
16854            $node.on('drop.drop-to-replace', function(e)
16855            {
16856                if (!self.app.isDragComponentInside())
16857                {
16858                    return self._setReplaceUpload(e, $node);
16859                }
16860            });
16861        });
16862    },
16863    _setFormData: function($modal, $form)
16864    {
16865        this._buildPreview($modal);
16866        this._buildPreviewUpload();
16867
16868        var imageData = this.$image.getData();
16869        var data = {
16870            title: imageData.title
16871        };
16872
16873        // caption
16874        if (this.opts.imageCaption) data.caption = imageData.caption;
16875        else $modal.find('.form-item-caption').hide();
16876
16877        // position
16878        if (this.opts.imagePosition) data.align = imageData.align;
16879        else $modal.find('.form-item-align').hide();
16880
16881        // link
16882        if (this.opts.imageLink)
16883        {
16884            if (imageData.link)
16885            {
16886                data.url = imageData.link.url;
16887                if (imageData.link.target) data.target = true;
16888            }
16889        }
16890        else $modal.find('.form-item-link').hide();
16891
16892        $form.setData(data);
16893    },
16894    _setFormFocus: function($form)
16895    {
16896        $form.getField('title').focus();
16897    },
16898    _setReplaceUpload: function(e, $node)
16899    {
16900        e = e.originalEvent || e;
16901        e.stopPropagation();
16902        e.preventDefault();
16903
16904        if (!this.opts.imageUpload) return;
16905
16906        this.$image = this.component.create('image', $node);
16907
16908        var options = {
16909            url: this.opts.imageUpload,
16910            files: e.dataTransfer.files,
16911            name: 'imagereplace',
16912            data: this.opts.imageData,
16913            paramName: this.opts.imageUploadParam
16914        };
16915
16916        this.app.api('module.upload.send', options);
16917
16918        return;
16919    },
16920    _setUpload: function($modal, $form)
16921    {
16922        if (!this.opts.imageUpload) {
16923            var $body = $modal.getBody();
16924            var $tab = $body.find('.redactor-modal-tab-upload');
16925            $tab.remove();
16926        }
16927
16928        var options = {
16929            url: this.opts.imageUpload,
16930            element: $form.getField('file'),
16931            name: 'image',
16932            data: this.opts.imageData,
16933            paramName: this.opts.imageUploadParam
16934        };
16935
16936        this.app.api('module.upload.build', options);
16937    },
16938    _buildPreview: function($modal)
16939    {
16940        this.$preview = $modal.find('#redactor-modal-image-preview');
16941
16942        var imageData = this.$image.getData();
16943        var $previewImg = $R.dom('<img>');
16944        $previewImg.attr('src', imageData.src);
16945
16946        this.$previewBox = $R.dom('<div>');
16947        this.$previewBox.append($previewImg);
16948
16949        this.$preview.html('');
16950        this.$preview.append(this.$previewBox);
16951    },
16952    _buildPreviewUpload: function()
16953    {
16954        if (!this.opts.imageUpload) return;
16955
16956        var $desc = $R.dom('<div class="desc">');
16957        $desc.html(this.lang.get('upload-change-label'));
16958
16959        this.$preview.append($desc);
16960
16961        var options = {
16962            url: this.opts.imageUpload,
16963            element: this.$previewBox,
16964            name: 'imageedit',
16965            data: this.opts.imageData,
16966            paramName: this.opts.imageUploadParam
16967        };
16968
16969        this.app.api('module.upload.build', options);
16970    }
16971});
16972$R.add('class', 'image.component', {
16973    mixins: ['dom', 'component'],
16974    init: function(app, el)
16975    {
16976        this.app = app;
16977        this.opts = app.opts;
16978        this.selection = app.selection;
16979
16980        // init
16981        return (el && el.cmnt !== undefined) ? el : this._init(el);
16982    },
16983    setData: function(data)
16984    {
16985        for (var name in data)
16986        {
16987            this._set(name, data[name]);
16988        }
16989    },
16990    getData: function()
16991    {
16992        var names = ['src', 'title', 'caption', 'align', 'link', 'id'];
16993        var data = {};
16994
16995        for (var i = 0; i < names.length; i++)
16996        {
16997            data[names[i]] = this._get(names[i]);
16998        }
16999
17000        return data;
17001    },
17002    getElement: function()
17003    {
17004        return this.$element;
17005    },
17006    changeImage: function(data)
17007    {
17008        this.$element.attr('src', data.url);
17009    },
17010
17011
17012    // private
17013    _init: function(el)
17014    {
17015        var $el = $R.dom(el);
17016        var $figure = $el.closest('figure');
17017
17018        if (el === undefined)
17019        {
17020            this.$element = $R.dom('<img>');
17021            this.parse('<figure>');
17022            this.append(this.$element);
17023        }
17024        else if ($figure.length === 0)
17025        {
17026            this.parse('<figure>');
17027            this.$element = $el;
17028            this.$element.wrap(this);
17029        }
17030        else
17031        {
17032            this.parse($figure);
17033            this.$element = this.find('img');
17034        }
17035
17036        this._initWrapper();
17037    },
17038    _set: function(name, value)
17039    {
17040        this['_set_' + name](value);
17041    },
17042    _get: function(name)
17043    {
17044        return this['_get_' + name]();
17045    },
17046    _set_src: function(src)
17047    {
17048       this.$element.attr('src', src);
17049    },
17050    _set_id: function(id)
17051    {
17052        if (this.opts.imageObserve) {
17053            this.$element.attr('data-image', id);
17054        }
17055    },
17056    _set_title: function(title)
17057    {
17058        title = title.trim().replace(/(<([^>]+)>)/ig,"");
17059
17060        if (title === '')
17061        {
17062            this.$element.removeAttr('alt');
17063        }
17064        else
17065        {
17066            this.$element.attr('alt', title);
17067        }
17068
17069    },
17070    _set_caption: function(caption)
17071    {
17072        var $figcaption = this.find('figcaption');
17073        if ($figcaption.length === 0)
17074        {
17075            $figcaption = $R.dom('<figcaption>');
17076            $figcaption.attr('contenteditable', 'true');
17077
17078            this.append($figcaption);
17079        }
17080
17081        if (caption === '') $figcaption.remove();
17082        else $figcaption.html(caption);
17083
17084        return $figcaption;
17085    },
17086    _set_align: function(align)
17087    {
17088        var imageFloat = '';
17089        var imageMargin = '';
17090        var textAlign = '';
17091        var $el = this;
17092        var $img = this.find('img');
17093        var $figcaption = this.find('figcaption');
17094
17095        if (typeof this.opts.imagePosition === 'object')
17096        {
17097            var positions = this.opts.imagePosition;
17098            for (var key in positions)
17099            {
17100                $el.removeClass(positions[key]);
17101            }
17102
17103            var alignClass = (typeof positions[align] !== 'undefined') ? positions[align] : false;
17104            if (alignClass)
17105            {
17106                $el.addClass(alignClass);
17107            }
17108        }
17109        else
17110        {
17111            var width = $img.width();
17112
17113            switch (align)
17114            {
17115
17116                case 'left':
17117                    imageFloat = 'left';
17118                    imageMargin = this.opts.imageFloatMargin;
17119                break;
17120                case 'right':
17121                    imageFloat = 'right';
17122                    imageMargin = this.opts.imageFloatMargin;
17123                break;
17124                case 'center':
17125                    textAlign = 'center';
17126                    imageMargin = 'auto';
17127                break;
17128            }
17129
17130            $el.css({ 'float': imageFloat, width: width + 'px', maxWidth: width + 'px', 'margin-left': imageMargin, 'margin-right': imageMargin, 'text-align': textAlign });
17131            $el.attr('rel', $el.attr('style'));
17132
17133            if (align === 'none') {
17134                $el.css('max-width', '');
17135                $el.css('width', '');
17136            }
17137
17138            if (align === 'center')
17139            {
17140                $el.css('max-width', '');
17141                $el.css('width', '');
17142                $figcaption.css('text-align', 'center');
17143            }
17144            else
17145            {
17146                $figcaption.css('text-align', '');
17147            }
17148        }
17149    },
17150    _set_link: function(data)
17151    {
17152        var $link = this._findLink();
17153        if (data.url === '')
17154        {
17155            if ($link) $link.unwrap();
17156
17157            return;
17158        }
17159
17160        if (!$link)
17161        {
17162            $link = $R.dom('<a>');
17163            this.$element.wrap($link);
17164        }
17165
17166        $link.attr('href', data.url);
17167
17168        if (data.target) $link.attr('target', (data.target === true) ? '_blank' : data.target);
17169        else $link.removeAttr('target');
17170
17171        return $link;
17172    },
17173    _get_src: function()
17174    {
17175        return this.$element.attr('src');
17176    },
17177    _get_id: function()
17178    {
17179        return this.$element.attr('data-image');
17180    },
17181    _get_title: function()
17182    {
17183        var alt = this.$element.attr('alt');
17184
17185        return (alt) ? alt : '';
17186    },
17187    _get_caption: function()
17188    {
17189        var $figcaption = this.find('figcaption');
17190
17191        if ($figcaption.length === 0)
17192        {
17193            return '';
17194        }
17195        else
17196        {
17197            return $figcaption.html();
17198        }
17199    },
17200    _get_align: function()
17201    {
17202        var align = '';
17203        if (typeof this.opts.imagePosition === 'object')
17204        {
17205            align = 'none';
17206            var positions = this.opts.imagePosition;
17207            for (var key in positions)
17208            {
17209                if (this.hasClass(positions[key]))
17210                {
17211                    align = key;
17212                    break;
17213                }
17214            }
17215        }
17216        else
17217        {
17218            align = (this.css('text-align') === 'center') ? 'center' : this.css('float');
17219        }
17220
17221        return align;
17222    },
17223    _get_link: function()
17224    {
17225        var $link = this._findLink();
17226        if ($link)
17227        {
17228            var target = ($link.attr('target')) ? true : false;
17229
17230            return {
17231                url: $link.attr('href'),
17232                target: target
17233            };
17234        }
17235    },
17236    _initWrapper: function()
17237    {
17238        this.addClass('redactor-component');
17239        this.attr({
17240            'data-redactor-type': 'image',
17241            'tabindex': '-1',
17242            'contenteditable': false
17243        });
17244    },
17245    _findLink: function()
17246    {
17247        var $link = this.find('a').filter(function(node)
17248        {
17249            return ($R.dom(node).closest('figcaption').length === 0);
17250        });
17251
17252        if ($link.length !== 0)
17253        {
17254            return $link;
17255        }
17256
17257        return false;
17258    }
17259});
17260$R.add('class', 'image.resize', {
17261    init: function(app)
17262    {
17263        this.app = app;
17264        this.$doc = app.$doc;
17265        this.$win = app.$win;
17266        this.$body = app.$body;
17267        this.editor = app.editor;
17268        this.toolbar = app.toolbar;
17269        this.inspector = app.inspector;
17270
17271        // init
17272        this.$target = (this.toolbar.isTarget()) ? this.toolbar.getTargetElement() : this.$body;
17273        this._init();
17274    },
17275    // public
17276    rebuild: function()
17277    {
17278        this._setResizerPosition();
17279    },
17280    hide: function()
17281    {
17282        this.$target.find('#redactor-image-resizer').remove();
17283    },
17284    stop: function()
17285    {
17286        var $editor = this.editor.getElement();
17287        $editor.off('.redactor.image-resize');
17288
17289        this.$doc.off('.redactor.image-resize');
17290        this.$win.off('resize.redactor.image-resize');
17291        this.hide();
17292    },
17293
17294    // private
17295    _init: function()
17296    {
17297        var $editor = this.editor.getElement();
17298        $editor.on('click.redactor.image-resize', this._build.bind(this));
17299
17300        this.$win.on('resize.redactor.image-resize', this._setResizerPosition.bind(this));
17301    },
17302    _build: function(e)
17303    {
17304        this.$target.find('#redactor-image-resizer').remove();
17305
17306        if (this.app.isReadOnly()) return;
17307
17308        var data = this.inspector.parse(e.target);
17309        var $editor = this.editor.getElement();
17310
17311        if (data.isComponentType('image'))
17312        {
17313            this.$resizableBox = $editor;
17314            this.$resizableImage = $R.dom(data.getImageElement());
17315
17316            this.$resizer = $R.dom('<span>');
17317            this.$resizer.attr('id', 'redactor-image-resizer');
17318
17319            this.$target.append(this.$resizer);
17320
17321            this._setResizerPosition();
17322            this.$resizer.on('mousedown touchstart', this._set.bind(this));
17323        }
17324    },
17325    _setResizerPosition: function()
17326    {
17327        if (this.$resizer)
17328        {
17329            var isTarget = this.toolbar.isTarget();
17330            var targetOffset = this.$target.offset();
17331            var offsetFix = 7;
17332            var topOffset = (isTarget) ? (offsetFix - targetOffset.top + this.$target.scrollTop()) : offsetFix;
17333            var leftOffset = (isTarget) ? (offsetFix - targetOffset.left) : offsetFix;
17334            var pos = this.$resizableImage.offset();
17335            var width = this.$resizableImage.width();
17336            var height = this.$resizableImage.height();
17337            var resizerWidth =  this.$resizer.width();
17338            var resizerHeight =  this.$resizer.height();
17339
17340            this.$resizer.css({ top: Math.round(pos.top + height - resizerHeight + topOffset) + 'px', left: Math.round(pos.left + width - resizerWidth + leftOffset) + 'px' });
17341        }
17342    },
17343    _set: function(e)
17344    {
17345        e.preventDefault();
17346
17347        this.resizeHandle = {
17348            x : e.pageX,
17349            y : e.pageY,
17350            el : this.$resizableImage,
17351            $figure: this.$resizableImage.closest('figure'),
17352            ratio: this.$resizableImage.width() / this.$resizableImage.height(),
17353            h: this.$resizableImage.height()
17354        };
17355
17356        e = e.originalEvent || e;
17357
17358        if (e.targetTouches)
17359        {
17360             this.resizeHandle.x = e.targetTouches[0].pageX;
17361             this.resizeHandle.y = e.targetTouches[0].pageY;
17362        }
17363
17364        this.app.broadcast('contextbar.close');
17365        this.app.broadcast('image.resize', this.$resizableImage);
17366        this._start();
17367    },
17368    _start: function()
17369    {
17370        this.$doc.on('mousemove.redactor.image-resize touchmove.redactor.image-resize', this._move.bind(this));
17371        this.$doc.on('mouseup.redactor.image-resize touchend.redactor.image-resize', this._stop.bind(this));
17372    },
17373    _stop: function()
17374    {
17375        this.$doc.off('.redactor.image-resize');
17376        this.app.broadcast('image.resized', this.$resizableImage);
17377    },
17378    _move: function(e)
17379    {
17380        e.preventDefault();
17381
17382        e = e.originalEvent || e;
17383
17384        var height = this.resizeHandle.h;
17385
17386        if (e.targetTouches) height += (e.targetTouches[0].pageY -  this.resizeHandle.y);
17387        else height += (e.pageY -  this.resizeHandle.y);
17388
17389        var width = height * this.resizeHandle.ratio;
17390
17391        width = Math.round(width);
17392        height = Math.round(height);
17393
17394        if (height < 20 || width < 100) return;
17395        if (this._getResizableBoxWidth() <= width) return;
17396
17397        if (this.resizeHandle.$figure.length !== 0 && this.resizeHandle.$figure.css('max-width') !== '') {
17398            this.resizeHandle.$figure.css({ width: width + 'px' , 'max-width': width + 'px' });
17399        }
17400
17401        this.resizeHandle.el.attr({ width: width, height: height });
17402        this.resizeHandle.el.width(width);
17403        this.resizeHandle.el.css('max-width', width + 'px');
17404        this.resizeHandle.el.height(height);
17405        this._setResizerPosition();
17406    },
17407    _getResizableBoxWidth: function()
17408    {
17409        var width = this.$resizableBox.width();
17410        return width - parseInt(this.$resizableBox.css('padding-left')) - parseInt(this.$resizableBox.css('padding-right'));
17411    }
17412});
17413$R.add('module', 'file', {
17414    modals: {
17415        'file':
17416            '<div class="redactor-modal-tab" data-title="## upload ##"><form action=""> \
17417                <div class="form-item form-item-title"> \
17418                    <label for="modal-file-title"> ## filename ## <span class="desc">(## optional ##)</span></label> \
17419                    <input type="text" id="modal-file-title" name="title" /> \
17420                </div> \
17421                <input type="file" name="file"> \
17422            </form></div>'
17423    },
17424    init: function(app)
17425    {
17426        this.app = app;
17427        this.opts = app.opts;
17428        this.lang = app.lang;
17429        this.caret = app.caret;
17430        this.utils = app.utils;
17431        this.storage = app.storage;
17432        this.component = app.component;
17433        this.inspector = app.inspector;
17434        this.insertion = app.insertion;
17435        this.selection = app.selection;
17436    },
17437    // messages
17438    onstarted: function()
17439    {
17440        // storage observe
17441        this.storage.observeFiles();
17442    },
17443    ondropfile: function(e, files, clipboard)
17444    {
17445        if (!this.opts.fileUpload) return;
17446
17447        var options = {
17448            url: this.opts.fileUpload,
17449            event: (clipboard) ? false : e,
17450            files: files,
17451            name: 'filedrop',
17452            data: this.opts.fileData
17453        };
17454
17455        this.app.api('module.upload.send', options);
17456    },
17457    onmodal: {
17458        file: {
17459            open: function($modal, $form)
17460            {
17461                this._setFormData($modal, $form);
17462                this._setUpload($form);
17463            },
17464            opened: function($modal, $form)
17465            {
17466                this._setFormFocus($form);
17467
17468                this.$form = $form;
17469            }
17470        }
17471    },
17472    onupload: {
17473        file: {
17474            complete: function(response)
17475            {
17476                this._insert(response);
17477            },
17478            error: function(response)
17479            {
17480                this._uploadError(response);
17481            }
17482        },
17483        filedrop: {
17484            complete: function(response, e)
17485            {
17486                this._insert(response, e);
17487            },
17488            error: function(response)
17489            {
17490                this._uploadError(response);
17491            }
17492        }
17493    },
17494
17495    // public
17496    open: function()
17497    {
17498        this._open();
17499    },
17500    insert: function(data)
17501    {
17502        this._insert(data);
17503    },
17504    remove: function(node)
17505    {
17506        this._remove(node);
17507    },
17508
17509    // private
17510    _open: function()
17511    {
17512        this.app.api('module.modal.build', this._getModalData());
17513    },
17514    _getModalData: function()
17515    {
17516        var modalData = {
17517            name: 'file',
17518            title: this.lang.get('file')
17519        };
17520
17521        return modalData;
17522    },
17523    _insert: function(response, e)
17524    {
17525        this.app.api('module.modal.close');
17526        if (typeof response !== 'object') return;
17527
17528        if (Array.isArray(response))
17529        {
17530            var obj = {};
17531            for (var i = 0; i < response.length; i++)
17532            {
17533                obj = $R.extend(obj, response[i]);
17534            }
17535
17536            response = obj;
17537        }
17538
17539        var multiple = (Object.keys(response).length  > 1);
17540
17541        if (multiple)
17542        {
17543            this._insertMultiple(response, e);
17544        }
17545        else
17546        {
17547            this._insertSingle(response, e);
17548        }
17549
17550        this.$form = false;
17551    },
17552    _insertSingle: function(response, e)
17553    {
17554        var inserted = [];
17555        for (var key in response)
17556        {
17557            var $file = this._createFileAndStore(response[key]);
17558
17559            if (this.opts.fileAttachment)
17560            {
17561                inserted = this._insertAsAttachment($file);
17562            }
17563            else
17564            {
17565                inserted = (e) ? this.insertion.insertToPoint(e, $file) : this.insertion.insertRaw($file);
17566            }
17567
17568            this.app.broadcast('file.uploaded', inserted[0], response);
17569        }
17570    },
17571    _insertMultiple: function(response, e)
17572    {
17573        var z = 0;
17574        var inserted = [];
17575        var $last;
17576        for (var key in response)
17577        {
17578            z++;
17579
17580            var $file = this._createFileAndStore(response[key]);
17581
17582            if (this.opts.fileAttachment)
17583            {
17584                inserted = this._insertAsAttachment($file, response);
17585            }
17586            else
17587            {
17588                if (z === 1)
17589                {
17590                    inserted = (e) ? this.insertion.insertToPoint(e, $file) : this.insertion.insertRaw($file);
17591                }
17592                else
17593                {
17594                    var $inserted = $R.dom(inserted[0]);
17595                    $inserted.after($file).after(' ');
17596                    inserted = [$file.get()];
17597
17598                    this.app.broadcast('file.inserted', $file);
17599                }
17600            }
17601
17602            $last = $file;
17603            this.app.broadcast('file.uploaded', inserted[0], response);
17604        }
17605
17606        // set caret after last
17607        if (!this.opts.fileAttachment)
17608        {
17609            this.caret.setAfter($last);
17610        }
17611    },
17612    _insertAsAttachment: function($file, response)
17613    {
17614        var $box = $R.dom(this.opts.fileAttachment);
17615        var $wrapper = $file.wrapAttachment();
17616        $box.append($wrapper);
17617
17618        var inserted = [$wrapper.get()];
17619        this.app.broadcast('file.appended', inserted[0], response);
17620
17621        return inserted;
17622    },
17623    _createFileAndStore: function(item)
17624    {
17625        var modalFormData = (this.$form) ? this.$form.getData() : false;
17626        var name = (item.name) ? item.name : item.url;
17627        var title = (!this.opts.fileAttachment && modalFormData && modalFormData.title !== '') ? modalFormData.title : this._truncateUrl(name);
17628
17629        var $file = this.component.create('file');
17630        $file.attr('href', item.url);
17631        $file.attr('data-file', (item.id) ? item.id : this.utils.getRandomId());
17632        $file.attr('data-name', item.name);
17633        $file.html(title);
17634
17635        // add to storage
17636        this.storage.add('file', $file);
17637
17638        return $file;
17639    },
17640    _remove: function(node)
17641    {
17642        this.selection.save();
17643
17644        var $file = this.component.create('file', node);
17645        var stop = this.app.broadcast('file.delete', $file);
17646        if (stop !== false)
17647        {
17648            $file.unwrap();
17649
17650            this.selection.restore();
17651
17652            // callback
17653            this.app.broadcast('file.deleted', $file);
17654        }
17655        else
17656        {
17657            this.selection.restore();
17658        }
17659    },
17660    _truncateUrl: function(url)
17661    {
17662        return (url.search(/^http/) !== -1 && url.length > 20) ? url.substring(0, 20) + '...' : url;
17663    },
17664    _setUpload: function($form)
17665    {
17666        var options = {
17667            url: this.opts.fileUpload,
17668            element: $form.getField('file'),
17669            name: 'file',
17670            data: this.opts.fileData,
17671            paramName: this.opts.fileUploadParam
17672        };
17673
17674        this.app.api('module.upload.build', options);
17675    },
17676    _setFormData: function($modal, $form)
17677    {
17678        if (this.opts.fileAttachment)
17679        {
17680            $modal.find('.form-item-title').hide();
17681        }
17682        else
17683        {
17684            $form.setData({ title: this.selection.getText() });
17685        }
17686    },
17687    _setFormFocus: function($form)
17688    {
17689        $form.getField('title').focus();
17690    },
17691    _uploadError: function(response)
17692    {
17693        this.app.broadcast('file.uploadError', response);
17694    }
17695});
17696$R.add('class', 'file.component', {
17697    mixins: ['dom', 'component'],
17698    init: function(app, el)
17699    {
17700        this.app = app;
17701        this.opts = app.opts;
17702
17703        // init
17704        return (el && el.cmnt !== undefined) ? el : this._init(el);
17705    },
17706    wrapAttachment: function()
17707    {
17708        this.$wrapper = $R.dom('<span class="redactor-file-item">');
17709        this.$remover = $R.dom('<span class="redactor-file-remover">');
17710        this.$remover.html('&times;');
17711        this.$remover.on('click', this.removeAttachment.bind(this));
17712
17713        this.$wrapper.append(this);
17714        this.$wrapper.append(this.$remover);
17715
17716        return this.$wrapper;
17717    },
17718    removeAttachment: function(e)
17719    {
17720        e.preventDefault();
17721
17722        var stop = this.app.broadcast('file.delete', this, this.$wrapper);
17723        if (stop !== false)
17724        {
17725            this.$wrapper.remove();
17726            this.app.broadcast('file.deleted', this);
17727            this.app.broadcast('file.removeAttachment', this);
17728        }
17729    },
17730
17731    // private
17732    _init: function(el)
17733    {
17734        if (el === undefined)
17735        {
17736            this.parse('<a>');
17737        }
17738        else
17739        {
17740            var $a = $R.dom(el).closest('a');
17741            this.parse($a);
17742        }
17743    }
17744});
17745$R.add('module', 'buffer', {
17746    init: function(app)
17747    {
17748        this.app = app;
17749        this.opts = app.opts;
17750        this.editor = app.editor;
17751        this.offset = app.offset;
17752        this.keycodes = app.keycodes;
17753        this.selection = app.selection;
17754
17755        // local
17756        this.state = false;
17757        this.passed = false;
17758        this.keyPressed = false;
17759        this.undoStorage = [];
17760        this.redoStorage = [];
17761    },
17762    // messages
17763    onkeydown: function(e)
17764    {
17765        this._listen(e);
17766    },
17767    onsyncing: function()
17768    {
17769        if (!this.keyPressed)
17770        {
17771            this.trigger();
17772        }
17773
17774        this.keyPressed = false;
17775    },
17776    onbuffer: {
17777        trigger: function()
17778        {
17779            this.trigger();
17780        }
17781    },
17782    onstate: function(e, html, offset)
17783    {
17784        if ((e && (e.ctrlKey || e.metaKey)) || (e && (this._isUndo(e) || this._isRedo(e))))
17785        {
17786            return;
17787        }
17788
17789        this.passed = false;
17790        this._saveState(html, offset);
17791
17792        if (e === false) {
17793            this._setUndo();
17794        }
17795    },
17796    onenable: function()
17797    {
17798        this.clear();
17799    },
17800
17801    // public
17802    clear: function()
17803    {
17804        this.state = false;
17805        this.undoStorage = [];
17806        this.redoStorage = [];
17807    },
17808    undo: function()
17809    {
17810        this._getUndo();
17811    },
17812    redo: function()
17813    {
17814        this._getRedo();
17815    },
17816    trigger: function()
17817    {
17818        if (this.state && this.passed === false) this._setUndo();
17819    },
17820
17821    // private
17822    _saveState: function(html, offset)
17823    {
17824        var $editor = this.editor.getElement();
17825
17826        this.state = {
17827            html: html || $editor.html(),
17828            offset: offset || this.offset.get()
17829        };
17830    },
17831    _listen: function(e)
17832    {
17833        var key = e.which;
17834        var ctrl = e.ctrlKey || e.metaKey;
17835        var cmd = ctrl || e.shiftKey || e.altKey;
17836        var keys = [this.keycodes.SPACE, this.keycodes.ENTER, this.keycodes.BACKSPACE, this.keycodes.DELETE, this.keycodes.TAB,
17837                    this.keycodes.LEFT, this.keycodes.RIGHT, this.keycodes.UP, this.keycodes.DOWN];
17838        // undo
17839        if (this._isUndo(e)) // z key
17840        {
17841            e.preventDefault();
17842            this.undo();
17843            return;
17844        }
17845        // redo
17846        else if (this._isRedo(e))
17847        {
17848            e.preventDefault();
17849            this.redo();
17850            return;
17851        }
17852        // spec keys
17853        else if (!ctrl && keys.indexOf(key) !== -1)
17854        {
17855            cmd = true;
17856            this.trigger();
17857        }
17858        // cut & copy
17859        else if (ctrl && (key === 88 || key === 67))
17860        {
17861            cmd = true;
17862            this.trigger();
17863        }
17864
17865
17866        // empty buffer
17867        if (!cmd && !this._hasUndo())
17868        {
17869            this.trigger();
17870        }
17871
17872        this.keyPressed = true;
17873    },
17874    _isUndo: function(e)
17875    {
17876        var key = e.which;
17877        var ctrl = e.ctrlKey || e.metaKey;
17878
17879        return (ctrl && key === 90 && !e.shiftKey && !e.altKey);
17880    },
17881    _isRedo: function(e)
17882    {
17883        var key = e.which;
17884        var ctrl = e.ctrlKey || e.metaKey;
17885
17886        return (ctrl && ((key === 90 && e.shiftKey) || (key === 89 && !e.shiftKey)) && !e.altKey);
17887    },
17888    _setUndo: function()
17889    {
17890        var last = this.undoStorage[this.undoStorage.length-1];
17891        if (typeof last === 'undefined' || last[0] !== this.state.html)
17892        {
17893            this.undoStorage.push([this.state.html, this.state.offset]);
17894            this._removeOverStorage();
17895        }
17896    },
17897    _setRedo: function()
17898    {
17899        var $editor = this.editor.getElement();
17900        var offset = this.offset.get();
17901        var html = $editor.html();
17902
17903        this.redoStorage.push([html, offset]);
17904        this.redoStorage = this.redoStorage.slice(0, this.opts.bufferLimit);
17905    },
17906    _getUndo: function()
17907    {
17908        if (!this._hasUndo()) return;
17909
17910        this.passed = true;
17911
17912        var $editor = this.editor.getElement();
17913        var buffer = this.undoStorage.pop();
17914
17915        this._setRedo();
17916
17917        $editor.html(buffer[0]);
17918        this.offset.set(buffer[1]);
17919        this._saveState(buffer[0], buffer[1]);
17920
17921        this.app.broadcast('undo', buffer[0], buffer[1]);
17922
17923    },
17924    _getRedo: function()
17925    {
17926        if (!this._hasRedo()) return;
17927
17928        this.passed = true;
17929
17930        var $editor = this.editor.getElement();
17931        var buffer = this.redoStorage.pop();
17932
17933        this._setUndo();
17934        $editor.html(buffer[0]);
17935        this.offset.set(buffer[1]);
17936        this._saveState(buffer[0], buffer[1]);
17937
17938        this.app.broadcast('redo', buffer[0], buffer[1]);
17939    },
17940    _removeOverStorage: function()
17941    {
17942        if (this.undoStorage.length > this.opts.bufferLimit)
17943        {
17944            this.undoStorage = this.undoStorage.slice(0, (this.undoStorage.length - this.opts.bufferLimit));
17945        }
17946    },
17947    _hasUndo: function()
17948    {
17949        return (this.undoStorage.length !== 0);
17950    },
17951    _hasRedo: function()
17952    {
17953        return (this.redoStorage.length !== 0);
17954    }
17955});
17956$R.add('module', 'list', {
17957    init: function(app)
17958    {
17959        this.app = app;
17960        this.uuid = app.uuid;
17961        this.opts = app.opts;
17962        this.utils = app.utils;
17963        this.block = app.block;
17964        this.editor = app.editor;
17965        this.toolbar = app.toolbar;
17966        this.inspector = app.inspector;
17967        this.selection = app.selection;
17968    },
17969    // messages
17970    onbutton: {
17971        list: {
17972            observe: function(button)
17973            {
17974                this._observeButton(button);
17975            }
17976        }
17977    },
17978    ondropdown: {
17979        list: {
17980            observe: function(dropdown)
17981            {
17982                this._observeDropdown(dropdown);
17983            }
17984        }
17985    },
17986
17987    // public
17988    toggle: function(type)
17989    {
17990        var nodes = this._getBlocks();
17991        var block = this.selection.getBlock();
17992        var $list = $R.dom(block).parents('ul, ol',  this.editor.getElement()).last();
17993        if (nodes.length === 0 && $list.length !== 0)
17994        {
17995            nodes = [$list.get()];
17996        }
17997
17998        if (block && (block.tagName === 'TD' || block.tagName === 'TH'))
17999        {
18000            nodes = this.block.format('div');
18001        }
18002
18003        this.selection.saveMarkers();
18004
18005        nodes = (nodes.length !== 0 && this._isUnformat(type, nodes)) ? this._unformat(type, nodes) : this._format(type, nodes);
18006
18007        this.selection.restoreMarkers();
18008
18009        return nodes;
18010    },
18011    indent: function()
18012    {
18013        var isCollapsed = this.selection.isCollapsed();
18014        var current = this.selection.getCurrent();
18015        var data = this.inspector.parse(current);
18016        var item = (data.isList()) ? data.getListItem() : false;
18017        var $item = $R.dom(item);
18018        var $prev = $item.prevElement();
18019        var prev = $prev.get();
18020        var isIndent = (isCollapsed && item && prev && prev.tagName === 'LI');
18021
18022        if (isIndent)
18023        {
18024            this.selection.saveMarkers();
18025
18026            $prev = $R.dom(prev);
18027            var $prevChild = $prev.children('ul, ol');
18028            var $list = $item.closest('ul, ol');
18029
18030            if ($prevChild.length !== 0)
18031            {
18032                $prevChild.append($item);
18033            }
18034            else
18035            {
18036                var listTag = $list.get().tagName.toLowerCase();
18037                var $newList = $R.dom('<' + listTag + '>');
18038
18039                $newList.append($item);
18040                $prev.append($newList);
18041            }
18042
18043            this.selection.restoreMarkers();
18044
18045            // empty list item
18046            if (this.utils.isEmptyHtml($item.html())) {
18047                this.app.caret.setStart($item);
18048            }
18049        }
18050    },
18051    outdent: function()
18052    {
18053        var isCollapsed = this.selection.isCollapsed();
18054        var current = this.selection.getCurrent();
18055        var data = this.inspector.parse(current);
18056        var item = (data.isList()) ? data.getListItem() : false;
18057        var $item = $R.dom(item);
18058
18059        if (isCollapsed && item)
18060        {
18061
18062            var $listItem = $item.parent();
18063            var $liItem = $listItem.closest('li', '.redactor-in-' + this.uuid);
18064            var $prev = $item.prevElement();
18065            var $next = $item.nextElement();
18066            var prev = $prev.get();
18067            var next = $next.get();
18068            var nextItems, nextList, $newList, $nextList;
18069            var isTop = (prev === false);
18070            var isMiddle = (prev !== false && next !== false);
18071            var isBottom = (!isTop && next === false);
18072
18073            this.selection.saveMarkers();
18074
18075            // out
18076            if ($liItem.length !== 0)
18077            {
18078                if (isMiddle)
18079                {
18080                    nextItems = this._getAllNext($item.get());
18081                    $newList = $R.dom('<' + $listItem.get().tagName.toLowerCase() + '>');
18082
18083                    for (var i = 0; i < nextItems.length; i++)
18084                    {
18085                        $newList.append(nextItems[i]);
18086                    }
18087
18088                    $liItem.after($item);
18089                    $item.append($newList);
18090                }
18091                else
18092                {
18093                    $liItem.after($item);
18094
18095                    if ($listItem.children().length === 0)
18096                    {
18097                        $listItem.remove();
18098                    }
18099                    else
18100                    {
18101                        if (isTop) $item.append($listItem);
18102                    }
18103                }
18104            }
18105            // unformat
18106            else
18107            {
18108                var $container =  this._createUnformatContainer($item);
18109                var $childList = $container.find('ul, ol').first();
18110
18111                if (isTop) $listItem.before($container);
18112                else if (isBottom) $listItem.after($container);
18113                else if (isMiddle)
18114                {
18115                    $newList = $R.dom('<' + $listItem.get().tagName.toLowerCase() + '>');
18116                    nextItems = this._getAllNext($item.get());
18117
18118                    for (var i = 0; i < nextItems.length; i++)
18119                    {
18120                        $newList.append(nextItems[i]);
18121                    }
18122
18123                    $listItem.after($container);
18124                    $container.after($newList);
18125                }
18126
18127                if ($childList.length !== 0)
18128                {
18129                    $nextList = $container.nextElement();
18130                    nextList = $nextList.get();
18131                    if (nextList && nextList.tagName === $listItem.get().tagName)
18132                    {
18133                        $R.dom(nextList).prepend($childList);
18134                        $childList.unwrap();
18135                    }
18136                    else
18137                    {
18138                        $container.after($childList);
18139                    }
18140                }
18141
18142                $item.remove();
18143            }
18144
18145            this.selection.restoreMarkers();
18146        }
18147    },
18148
18149    // private
18150    _getAllNext: function(next)
18151    {
18152        var nodes = [];
18153
18154        while (next)
18155        {
18156            var $next = $R.dom(next).nextElement();
18157            next = $next.get();
18158
18159            if (next) nodes.push(next);
18160            else return nodes;
18161        }
18162
18163        return nodes;
18164    },
18165    _isUnformat: function(type, nodes)
18166    {
18167        var countLists = 0;
18168        for (var i = 0; i < nodes.length; i++)
18169        {
18170            if (nodes[i].nodeType !== 3)
18171            {
18172                var tag = nodes[i].tagName.toLowerCase();
18173                if (tag === type || tag === 'figure')
18174                {
18175                    countLists++;
18176                }
18177            }
18178        }
18179
18180        return (countLists === nodes.length);
18181    },
18182    _format: function(type, nodes)
18183    {
18184        var tags = ['p', 'div', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol'];
18185        var blocks = this._uniteBlocks(nodes, tags);
18186        var lists = [];
18187
18188        for (var key in blocks)
18189        {
18190            var items = blocks[key];
18191            var $list = this._createList(type, blocks[key]);
18192
18193            for (var i = 0; i < items.length; i++)
18194            {
18195                var $item;
18196
18197                // lists
18198                if (items[i].nodeType !== 3 && (items[i].tagName === 'UL' || items[i].tagName === 'OL'))
18199                {
18200                    var $oldList = $R.dom(items[i]);
18201
18202                    $item = $oldList.contents();
18203                    $list.append($item);
18204
18205                    // old is empty
18206                    if (this.utils.isEmpty($oldList)) $oldList.remove();
18207                }
18208                // other blocks or texts
18209                else
18210                {
18211                    $item = this._createListItem(items[i]);
18212                    var last = $item.get().lastChild;
18213                    if (last && last.tagName === 'BR') {
18214                        $R.dom(last).remove();
18215                    }
18216
18217                    this.utils.normalizeTextNodes($item);
18218                    $list.append($item);
18219                }
18220            }
18221
18222            lists.push($list.get());
18223        }
18224
18225        return lists;
18226    },
18227    _uniteBlocks: function(nodes, tags)
18228    {
18229        var z = 0;
18230        var blocks = { 0: [] };
18231        var lastcell = false;
18232        for (var i = 0; i < nodes.length; i++)
18233        {
18234            var $node = $R.dom(nodes[i]);
18235            var $cell = $node.closest('th, td');
18236
18237            if ($cell.length !== 0)
18238            {
18239                if ($cell.get() !== lastcell)
18240                {
18241                    // create block
18242                    z++;
18243                    blocks[z] = [];
18244                }
18245
18246                if (this._isUniteBlock(nodes[i], tags))
18247                {
18248                    blocks[z].push(nodes[i]);
18249                }
18250            }
18251            else
18252            {
18253                if (this._isUniteBlock(nodes[i], tags))
18254                {
18255                    blocks[z].push(nodes[i]);
18256                }
18257                else
18258                {
18259                    // create block
18260                    z++;
18261                    blocks[z] = [];
18262                }
18263            }
18264
18265            lastcell = $cell.get();
18266        }
18267
18268        return blocks;
18269    },
18270    _isUniteBlock: function(node, tags)
18271    {
18272        return (node.nodeType === 3 || tags.indexOf(node.tagName.toLowerCase()) !== -1);
18273    },
18274    _createList: function(type, blocks)
18275    {
18276        var last = blocks[blocks.length-1];
18277        var $last = $R.dom(last);
18278        var $list = $R.dom('<' + type + '>');
18279        $last.after($list);
18280
18281        return $list;
18282    },
18283    _createListItem: function(item)
18284    {
18285        var $item = $R.dom('<li>');
18286        if (item.nodeType === 3)
18287        {
18288            $item.append(item);
18289        }
18290        else
18291        {
18292            var $el = $R.dom(item);
18293            $item.append($el.contents());
18294            $el.remove();
18295        }
18296
18297        return $item;
18298    },
18299    _unformat: function(type, nodes)
18300    {
18301        if (nodes.length === 1)
18302        {
18303            // one list
18304            var $list = $R.dom(nodes[0]);
18305            var $items = $list.find('li');
18306
18307            var selectedItems = this.selection.getNodes({ tags: ['li'] });
18308            var block = this.selection.getBlock();
18309            var $li = $R.dom(block).closest('li');
18310            if (selectedItems.length === 0 && $li.length !== 0)
18311            {
18312                selectedItems = [$li.get()];
18313            }
18314
18315
18316            // 1) entire
18317            if (selectedItems.length === $items.length)
18318            {
18319                return this._unformatEntire(nodes[0]);
18320            }
18321
18322            var pos = this._getItemsPosition($items, selectedItems);
18323
18324            // 2) top
18325            if (pos === 'Top')
18326            {
18327                return this._unformatAtSide('before', selectedItems, $list);
18328            }
18329
18330            // 3) bottom
18331            else if (pos === 'Bottom')
18332            {
18333                selectedItems.reverse();
18334                return this._unformatAtSide('after', selectedItems, $list);
18335            }
18336
18337            // 4) middle
18338            else if (pos === 'Middle')
18339            {
18340                var $last = $R.dom(selectedItems[selectedItems.length-1]);
18341
18342                var ci = false;
18343
18344                var $parent = false;
18345                var $secondList = $R.dom('<' + $list.get().tagName.toLowerCase() + '>');
18346                $items.each(function(node)
18347                {
18348                    if (ci)
18349                    {
18350                        var $node = $R.dom(node);
18351                        if ($node.closest('.redactor-split-item').length === 0 && ($parent === false || $node.closest($parent).length === 0))
18352                        {
18353                            $node.addClass('redactor-split-item');
18354                        }
18355
18356                        $parent = $node;
18357                    }
18358
18359                    if (node === $last.get())
18360                    {
18361                        ci = true;
18362                    }
18363                });
18364
18365                $items.filter('.redactor-split-item').each(function(node)
18366                {
18367                    var $node = $R.dom(node);
18368                    $node.removeClass('redactor-split-item');
18369                    $secondList.append(node);
18370                });
18371
18372                $list.after($secondList);
18373
18374                selectedItems.reverse();
18375                for (var i = 0; i < selectedItems.length; i++)
18376                {
18377                    var $item = $R.dom(selectedItems[i]);
18378                    var $container = this._createUnformatContainer($item);
18379
18380                    $list.after($container);
18381                    $container.find('ul, ol').remove();
18382                    $item.remove();
18383                }
18384
18385
18386                return;
18387            }
18388
18389        }
18390        else
18391        {
18392            // unformat all
18393            for (var i = 0; i < nodes.length; i++)
18394            {
18395                if (nodes[i].nodeType !== 3 && nodes[i].tagName.toLowerCase() === type)
18396                {
18397                    this._unformatEntire(nodes[i]);
18398                }
18399            }
18400        }
18401    },
18402    _unformatEntire: function(list)
18403    {
18404        var $list = $R.dom(list);
18405        var $items = $list.find('li');
18406        $items.each(function(node)
18407        {
18408            var $item = $R.dom(node);
18409            var $container = this._createUnformatContainer($item);
18410
18411            $item.remove();
18412            $list.before($container);
18413
18414        }.bind(this));
18415
18416        $list.remove();
18417    },
18418    _unformatAtSide: function(type, selectedItems, $list)
18419    {
18420        for (var i = 0; i < selectedItems.length; i++)
18421        {
18422            var $item = $R.dom(selectedItems[i]);
18423            var $container = this._createUnformatContainer($item);
18424
18425            $list[type]($container);
18426
18427            var $innerLists = $container.find('ul, ol').first();
18428            $item.append($innerLists);
18429
18430            $innerLists.each(function(node)
18431            {
18432                var $node = $R.dom(node);
18433                var $parent = $node.closest('li');
18434
18435                if ($parent.get() === selectedItems[i])
18436                {
18437                    $node.unwrap();
18438                    $parent.addClass('r-unwrapped');
18439                }
18440
18441            });
18442
18443            if (this.utils.isEmptyHtml($item.html())) $item.remove();
18444        }
18445
18446        // clear empty
18447        $list.find('.r-unwrapped').each(function(node)
18448        {
18449            var $node = $R.dom(node);
18450            if ($node.html().trim() === '') $node.remove();
18451            else $node.removeClass('r-unwrapped');
18452        });
18453    },
18454    _getItemsPosition: function($items, selectedItems)
18455    {
18456        var pos = 'Middle';
18457
18458        var sFirst = selectedItems[0];
18459        var sLast = selectedItems[selectedItems.length-1];
18460
18461        var first = $items.first().get();
18462        var last = $items.last().get();
18463
18464        if (first === sFirst && last !== sLast)
18465        {
18466            pos = 'Top';
18467        }
18468        else if (first !== sFirst && last === sLast)
18469        {
18470            pos = 'Bottom';
18471        }
18472
18473        return pos;
18474    },
18475    _createUnformatContainer: function($item)
18476    {
18477        var $container = $R.dom('<' + this.opts.markup + '>');
18478        if (this.opts.breakline) $container.attr('data-redactor-tag', 'br');
18479
18480        $container.append($item.contents());
18481
18482        return $container;
18483    },
18484    _getBlocks: function()
18485    {
18486        return this.selection.getBlocks({ first: true });
18487    },
18488    _observeButton: function()
18489    {
18490        var current = this.selection.getCurrent();
18491        var data = this.inspector.parse(current);
18492        var isDisabled = (data.isPre() || data.isCode() || data.isFigcaption());
18493
18494        this._observeButtonsList(isDisabled, ['lists', 'ul', 'ol', 'outdent', 'indent']);
18495
18496        var itemOutdent = this.toolbar.getButton('outdent');
18497        var itemIndent = this.toolbar.getButton('indent');
18498
18499        this._observeIndent(itemIndent, itemOutdent);
18500    },
18501    _observeDropdown: function(dropdown)
18502    {
18503        var itemOutdent = dropdown.getItem('outdent');
18504        var itemIndent = dropdown.getItem('indent');
18505
18506        this._observeIndent(itemIndent, itemOutdent);
18507    },
18508    _observeIndent: function(itemIndent, itemOutdent)
18509    {
18510        var isCollapsed = this.selection.isCollapsed();
18511        var current = this.selection.getCurrent();
18512        var data = this.inspector.parse(current);
18513        var item = (data.isList()) ? data.getListItem() : false;
18514        var $item = $R.dom(item);
18515        var $prev = $item.prevElement();
18516        var prev = $prev.get();
18517        var isIndent = (isCollapsed && item && prev && prev.tagName === 'LI');
18518
18519        if (itemOutdent)
18520        {
18521            if (item && isCollapsed) itemOutdent.enable();
18522            else itemOutdent.disable();
18523        }
18524
18525        if (itemIndent)
18526        {
18527            if (item && isIndent) itemIndent.enable();
18528            else itemIndent.disable();
18529        }
18530    },
18531    _observeButtonsList: function(param, buttons)
18532    {
18533        for (var i = 0; i < buttons.length; i++)
18534        {
18535            var button = this.toolbar.getButton(buttons[i]);
18536            if (button)
18537            {
18538                if (param) button.disable();
18539                else button.enable();
18540            }
18541        }
18542    }
18543});
18544$R.add('class', 'video.component', {
18545    mixins: ['dom', 'component'],
18546    init: function(app, el)
18547    {
18548        this.app = app;
18549
18550        // init
18551        return (el && el.cmnt !== undefined) ? el : this._init(el);
18552    },
18553
18554    // private
18555    _init: function(el)
18556    {
18557        if (typeof el !== 'undefined')
18558        {
18559            var $node = $R.dom(el);
18560            var $wrapper = $node.closest('figure');
18561            if ($wrapper.length !== 0)
18562            {
18563                this.parse($wrapper);
18564            }
18565            else
18566            {
18567                this.parse('<figure>');
18568                this.append(el);
18569            }
18570        }
18571        else
18572        {
18573            this.parse('<figure>');
18574        }
18575
18576
18577        this._initWrapper();
18578    },
18579    _initWrapper: function()
18580    {
18581        this.addClass('redactor-component');
18582        this.attr({
18583            'data-redactor-type': 'video',
18584            'tabindex': '-1',
18585            'contenteditable': false
18586        });
18587    }
18588});
18589
18590$R.add('class', 'widget.component', {
18591    mixins: ['dom', 'component'],
18592    init: function(app, el)
18593    {
18594        this.app = app;
18595
18596        // init
18597        return (el && el.cmnt !== undefined) ? el : this._init(el);
18598    },
18599    getData: function()
18600    {
18601        return {
18602            html: this._getHtml()
18603        };
18604    },
18605
18606    // private
18607    _init: function(el)
18608    {
18609        if (typeof el !== 'undefined')
18610        {
18611            var $node = $R.dom(el);
18612            var $figure = $node.closest('figure');
18613            if ($figure.length !== 0)
18614            {
18615                this.parse($figure);
18616            }
18617            else
18618            {
18619                this.parse('<figure>');
18620                this.html(el);
18621            }
18622        }
18623        else
18624        {
18625            this.parse('<figure>');
18626        }
18627
18628
18629        this._initWrapper();
18630    },
18631    _getHtml: function()
18632    {
18633        var $wrapper = $R.dom('<div>');
18634        $wrapper.html(this.html());
18635        $wrapper.find('.redactor-component-caret').remove();
18636
18637        return $wrapper.html();
18638    },
18639    _initWrapper: function()
18640    {
18641        this.addClass('redactor-component');
18642        this.attr({
18643            'data-redactor-type': 'widget',
18644            'tabindex': '-1',
18645            'contenteditable': false
18646        });
18647    }
18648});
18649
18650    var Redactor = $R;
18651    window.Redactor = window.$R = $R;
18652
18653    // Data attribute load
18654    window.addEventListener('load', function()
18655    {
18656        $R('[data-redactor]');
18657    });
18658
18659    // Export for webpack
18660    if (typeof module === 'object' && module.exports) {
18661        module.exports = Redactor;
18662        module.exports.Redactor = Redactor;
18663    }
18664}());

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.