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: '• ## 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: '• ## 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?\/?>|| )$/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 ¤ entity in the links 5655 html = html.replace(/¤t/gi, '¤t'); 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, '$'); 5696 html = html.replace(/&/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(/$/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(/&/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> <\/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(' ', ' '); 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(' ', ' '); 6092 }, 6093 6094 // remove 6095 removeInvisibleSpaces: function(html) 6096 { 6097 html = this.utils.removeInvisibleChars(html); 6098 html = html.replace(//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(/ /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, '$'); 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, '< '); 6210 html = html.replace(/<([^>]+)</gi, '<$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(/ /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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); 6277 6278 return str; 6279 }, 6280 encodePhpCode: function(html) 6281 { 6282 html = html.replace('<?php', '<?php'); 6283 html = html.replace('<?', '<?'); 6284 html = html.replace('?>', '?>'); 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(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/&/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(/ /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, '$'); 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(/ /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, '$'); 9626 html = html.replace(/&/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(/ /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(/ /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, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"); 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('<?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(' '); 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('×'); 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.