1 /** 2 * SCEditor BBCode Plugin 3 * http://www.samclarke.com/2011/07/sceditor/ 4 * 5 * Copyright (C) 2011-2012, Sam Clarke (samclarke.com) 6 * 7 * SCEditor is dual licensed under the MIT and GPL licenses: 8 * http://www.opensource.org/licenses/mit-license.php 9 * http://www.gnu.org/licenses/gpl.html 10 * 11 * @author Sam Clarke 12 * @version 1.3.5 13 * @requires jQuery 14 */ 15 16 // ==ClosureCompiler== 17 // @output_file_name jquery.sceditor.min.js 18 // @compilation_level SIMPLE_OPTIMIZATIONS 19 // ==/ClosureCompiler== 20 21 /*jshint smarttabs: true, jquery: true, eqnull:true, curly: false */ 22 23 (function($) { 24 'use strict'; 25 26 /** 27 * BBCode plugin for SCEditor 28 * 29 * @param {Element} el The textarea to be converted 30 * @return {Object} options 31 * @class sceditorBBCodePlugin 32 * @name jQuery.sceditorBBCodePlugin 33 */ 34 $.sceditorBBCodePlugin = function(element, options) { 35 var base = this; 36 37 /** 38 * Private methods 39 * @private 40 */ 41 var init, 42 buildBbcodeCache, 43 handleStyles, 44 handleTags, 45 formatString, 46 getStyle, 47 wrapInDivs, 48 mergeTextModeCommands; 49 50 base.bbcodes = $.sceditorBBCodePlugin.bbcodes; 51 52 53 /** 54 * cache of all the tags pointing to their bbcodes to enable 55 * faster lookup of which bbcode a tag should have 56 * @private 57 */ 58 var tagsToBbcodes = {}; 59 60 /** 61 * Same as tagsToBbcodes but instead of HTML tags it's styles 62 * @private 63 */ 64 var stylesToBbcodes = {}; 65 66 /** 67 * Allowed children of specific HTML tags. Empty array if no 68 * children other than text nodes are allowed 69 * @private 70 */ 71 var validChildren = { 72 ul: ['li'], 73 ol: ['li'], 74 table: ['tr'], 75 tr: ['td', 'th'], 76 code: ['br', 'p', 'div'], 77 youtube: [] 78 }; 79 80 81 /** 82 * Initializer 83 * @private 84 * @name sceditorBBCodePlugin.init 85 */ 86 init = function() { 87 $.data(element, "sceditorbbcode", base); 88 89 base.options = $.extend({}, $.sceditor.defaultOptions, options); 90 91 // build the BBCode cache 92 buildBbcodeCache(); 93 94 (new $.sceditor(element, 95 $.extend({}, base.options, { 96 getHtmlHandler: base.getHtmlHandler, 97 getTextHandler: base.getTextHandler, 98 commands: mergeTextModeCommands() 99 }) 100 )); 101 }; 102 103 mergeTextModeCommands = function() { 104 var merge = { 105 bold: { txtExec: ["[b]", "[/b]"] }, 106 italic: { txtExec: ["[i]", "[/i]"] }, 107 underline: { txtExec: ["[u]", "[/u]"] }, 108 strike: { txtExec: ["[s]", "[/s]"] }, 109 subscript: { txtExec: ["[sub]", "[/sub]"] }, 110 superscript: { txtExec: ["[sup]", "[/sup]"] }, 111 left: { txtExec: ["[left]", "[/left]"] }, 112 center: { txtExec: ["[center]", "[/center]"] }, 113 right: { txtExec: ["[right]", "[/right]"] }, 114 justify: { txtExec: ["[justify]", "[/justify]"] }, 115 font: { txtExec: function(caller) { 116 var editor = this; 117 118 $.sceditor.command.get('font')._createDropDown( 119 editor, 120 caller, 121 function(fontName) { 122 editor.insertText("[font="+fontName+"]", "[/font]"); 123 } 124 ); 125 } }, 126 size: { txtExec: function(caller) { 127 var editor = this; 128 129 $.sceditor.command.get('size')._createDropDown( 130 editor, 131 caller, 132 function(fontSize) { 133 editor.insertText("[size="+fontSize+"]", "[/size]"); 134 } 135 ); 136 } }, 137 color: { txtExec: function(caller) { 138 var editor = this; 139 140 $.sceditor.command.get('color')._createDropDown( 141 editor, 142 caller, 143 function(color) { 144 editor.insertText("[color="+color+"]", "[/color]"); 145 } 146 ); 147 } }, 148 bulletlist: { txtExec: ["[ul][li]", "[/li][/ul]"] }, 149 orderedlist: { txtExec: ["[ol][li]", "[/li][/ol]"] }, 150 table: { txtExec: ["[table][tr][td]", "[/td][/tr][/table]"] }, 151 horizontalrule: { txtExec: ["[hr]"] }, 152 code: { txtExec: ["[code]", "[/code]"] }, 153 image: { txtExec: function(caller, selected) { 154 var url = prompt(this._("Enter the image URL:"), selected); 155 156 if(url) 157 this.insertText("[img]" + url + "[/img]"); 158 } }, 159 email: { txtExec: function(caller, selected) { 160 var email = prompt(this._("Enter the e-mail address:"), selected || "@"), 161 text = prompt(this._("Enter the displayed text:"), email) || email; 162 163 if(email) 164 this.insertText("[email=" + email + "]" + text + "[/email]"); 165 } }, 166 link: { txtExec: function(caller, selected) { 167 var url = prompt(this._("Enter URL:"), selected || "http://"), 168 text = prompt(this._("Enter the displayed text:"), url) || url; 169 170 if(url) 171 this.insertText("[url=" + url + "]" + text + "[/url]"); 172 } }, 173 quote: { txtExec: ["[quote]", "[/quote]"] }, 174 youtube: { txtExec: function(caller, selected) { 175 var url = prompt(this._("Enter the YouTube video URL or ID:"), selected); 176 177 if(url) 178 { 179 if(url.indexOf("://") > -1) 180 url = url.replace(/^[^v]+v.(.{11}).*/,"$1"); 181 182 this.insertText("[youtube]" + url + "[/youtube]"); 183 } 184 } }, 185 rtl: { txtExec: ["[rtl]", "[/rtl]"] }, 186 ltr: { txtExec: ["[ltr]", "[/ltr]"] } 187 }; 188 189 return $.extend(true, {}, merge, $.sceditor.commands); 190 }; 191 192 /** 193 * Populates tagsToBbcodes and stylesToBbcodes to enable faster lookups 194 * 195 * @private 196 */ 197 buildBbcodeCache = function() { 198 $.each(base.bbcodes, function(bbcode, info) { 199 if(typeof base.bbcodes[bbcode].tags !== "undefined") 200 $.each(base.bbcodes[bbcode].tags, function(tag, values) { 201 var isBlock = !!base.bbcodes[bbcode].isBlock; 202 tagsToBbcodes[tag] = (tagsToBbcodes[tag] || {}); 203 tagsToBbcodes[tag][isBlock] = (tagsToBbcodes[tag][isBlock] || {}); 204 tagsToBbcodes[tag][isBlock][bbcode] = values; 205 }); 206 207 if(typeof base.bbcodes[bbcode].styles !== "undefined") 208 $.each(base.bbcodes[bbcode].styles, function(style, values) { 209 var isBlock = !!base.bbcodes[bbcode].isBlock; 210 stylesToBbcodes[isBlock] = (stylesToBbcodes[isBlock] || {}); 211 stylesToBbcodes[isBlock][style] = (stylesToBbcodes[isBlock][style] || {}); 212 stylesToBbcodes[isBlock][style][bbcode] = values; 213 }); 214 }); 215 }; 216 217 getStyle = function(element, property) { 218 var name = $.camelCase(property), 219 $elm, ret, dir; 220 221 // add exception for align 222 if("text-align" === property) 223 { 224 $elm = $(element); 225 226 if($elm.parent().css(property) !== $elm.css(property) && 227 $elm.css('display') === "block" && !$elm.is('hr') && !$elm.is('th')) 228 ret = $elm.css(property); 229 230 // IE changes text-align to the same as direction so skip unless overried by user 231 dir = element.style['direction']; 232 if(dir && ((/right/i.test(ret) && dir === 'rtl') || (/left/i.test(ret) && dir === 'ltr'))) 233 return null; 234 235 return ret; 236 } 237 238 if(element.style) 239 return element.style[name]; 240 241 return null; 242 }; 243 244 /** 245 * Checks if any bbcode styles match the elements styles 246 * 247 * @private 248 * @return string Content with any matching bbcode tags wrapped around it. 249 * @Private 250 */ 251 handleStyles = function(element, content, blockLevel) { 252 var elementPropVal, 253 tag = element[0].nodeName.toLowerCase(); 254 255 // convert blockLevel to boolean 256 blockLevel = !!blockLevel; 257 258 if(!stylesToBbcodes[blockLevel]) 259 return content; 260 261 $.each(stylesToBbcodes[blockLevel], function(property, bbcodes) { 262 elementPropVal = getStyle(element[0], property); 263 if(elementPropVal == null || elementPropVal === "") 264 return; 265 266 // if the parent has the same style use that instead of this one 267 // so you dont end up with [i]parent[i]child[/i][/i] 268 if(getStyle(element.parent()[0], property) === elementPropVal) 269 return; 270 271 $.each(bbcodes, function(bbcode, values) { 272 if((element[0].childNodes.length === 0 || element[0].childNodes[0].nodeName.toLowerCase() === "br") && 273 !base.bbcodes[bbcode].allowsEmpty) 274 return; 275 276 if(values === null || $.inArray(elementPropVal.toString(), values) > -1) { 277 if($.isFunction(base.bbcodes[bbcode].format)) 278 content = base.bbcodes[bbcode].format.call(base, element, content); 279 else 280 content = formatString(base.bbcodes[bbcode].format, content); 281 } 282 }); 283 }); 284 285 return content; 286 }; 287 288 /** 289 * Handles a HTML tag and finds any matching bbcodes 290 * 291 * @private 292 * @param jQuery element element The element to convert 293 * @param string content The Tags text content 294 * @param bool blockLevel If to convert block level tags 295 * @return string Content with any matching bbcode tags wrapped around it. 296 * @Private 297 */ 298 handleTags = function(element, content, blockLevel) { 299 var tag = element[0].nodeName.toLowerCase(); 300 301 // convert blockLevel to boolean 302 blockLevel = !!blockLevel; 303 304 if(tagsToBbcodes[tag] && tagsToBbcodes[tag][blockLevel]) { 305 // loop all bbcodes for this tag 306 $.each(tagsToBbcodes[tag][blockLevel], function(bbcode, bbcodeAttribs) { 307 if(!base.bbcodes[bbcode].allowsEmpty && 308 (element[0].childNodes.length === 0 || (element[0].childNodes[0].nodeName.toLowerCase() === "br" && element[0].childNodes.length === 1)) ) 309 return; 310 311 // if the bbcode requires any attributes then check this has 312 // all needed 313 if(bbcodeAttribs !== null) { 314 var runBbcode = false; 315 316 // loop all the bbcode attribs 317 $.each(bbcodeAttribs, function(attrib, values) 318 { 319 // check if has the bbcodes attrib 320 if(element.attr(attrib) == null) 321 return; 322 323 // if the element has the bbcodes attribute and the bbcode attribute 324 // has values check one of the values matches 325 if(values !== null && $.inArray(element.attr(attrib), values) < 0) 326 return; 327 328 // break this loop as we have matched this bbcode 329 runBbcode = true; 330 return false; 331 }); 332 333 if(!runBbcode) 334 return; 335 } 336 337 if($.isFunction(base.bbcodes[bbcode].format)) 338 content = base.bbcodes[bbcode].format.call(base, element, content); 339 else 340 content = formatString(base.bbcodes[bbcode].format, content); 341 }); 342 } 343 344 // add newline after paragraph elements p and div (WebKit uses divs) and br tags 345 if(blockLevel && /^(br|div|p)$/.test(tag)) 346 { 347 var parentChildren = element[0].parentNode.childNodes; 348 349 // if it's a <p><br /></p> the paragraph will put the newline so skip the br 350 if(!("br" === tag && parentChildren.length === 1) && 351 !("br" === tag && parentChildren[parentChildren.length-1] === element[0])) { 352 content += "\n"; 353 } 354 355 // needed for browsers that enter textnode then when return is pressed put the rest in a div, i.e.: 356 // text<div>line 2</div> 357 if("br" !== tag && !$.sceditor.dom.isInline(element.parent()[0]) && element[0].previousSibling && 358 element[0].previousSibling.nodeType === 3) { 359 content = "\n" + content; 360 } 361 } 362 363 return content; 364 }; 365 366 /** 367 * Formats a string in the format 368 * {0}, {1}, {2}, ect. with the params provided 369 * @private 370 * @return string 371 * @Private 372 */ 373 formatString = function() { 374 var args = arguments; 375 return args[0].replace(/\{(\d+)\}/g, function(str, p1) { 376 return typeof args[p1-0+1] !== "undefined"? 377 args[p1-0+1] : 378 '{' + p1 + '}'; 379 }); 380 }; 381 382 /** 383 * Removes any leading or trailing quotes ('") 384 * 385 * @return string 386 * @memberOf jQuery.sceditorBBCodePlugin.prototype 387 */ 388 base.stripQuotes = function(str) { 389 return str.replace(/^["']+/, "").replace(/["']+$/, ""); 390 }; 391 392 /** 393 * Converts HTML to BBCode 394 * @param string html Html string, this function ignores this, it works off domBody 395 * @param HtmlElement domBody Editors dom body object to convert 396 * @return string BBCode which has been converted from HTML 397 * @memberOf jQuery.sceditorBBCodePlugin.prototype 398 */ 399 base.getHtmlHandler = function(html, domBody) { 400 $.sceditor.dom.removeWhiteSpace(domBody[0]); 401 402 return $.trim(base.elementToBbcode(domBody)); 403 }; 404 405 /** 406 * Converts a HTML dom element to BBCode starting from 407 * the innermost element and working backwards 408 * 409 * @private 410 * @param HtmlElement element The element to convert to BBCode 411 * @param array vChildren Valid child tags allowed 412 * @return string BBCode 413 * @memberOf jQuery.sceditorBBCodePlugin.prototype 414 */ 415 base.elementToBbcode = function($element) { 416 return (function toBBCode(node, vChildren) { 417 var ret = ''; 418 419 $.sceditor.dom.traverse(node, function(node) { 420 var $node = $(node), 421 curTag = '', 422 tag = node.nodeName.toLowerCase(), 423 vChild = validChildren[tag], 424 isValidChild = true; 425 426 if(typeof vChildren === 'object') 427 { 428 isValidChild = $.inArray(tag, vChildren) > -1; 429 430 // if this tag is one of the parents allowed children 431 // then set this tags allowed children to whatever it allows, 432 // otherwise set to what the parent allows 433 if(!isValidChild) 434 vChild = vChildren; 435 } 436 437 // 3 is text element 438 if(node.nodeType !== 3) 439 { 440 // skip ignored elments 441 if($node.hasClass("sceditor-ignore")) 442 return; 443 444 // don't loop inside iframes 445 if(tag !== 'iframe') 446 curTag = toBBCode(node, vChild); 447 448 if(isValidChild) 449 { 450 // code tags should skip most styles 451 if(!$node.is('code')) 452 { 453 // handle inline bbcodes 454 curTag = handleStyles($node, curTag); 455 curTag = handleTags($node, curTag); 456 457 // handle blocklevel bbcodes 458 curTag = handleStyles($node, curTag, true); 459 } 460 461 ret += handleTags($node, curTag, true); 462 } 463 else 464 ret += curTag; 465 } 466 else if(node.wholeText && (!node.previousSibling || node.previousSibling.nodeType !== 3)) 467 { 468 if($(node).parents('code').length === 0) 469 ret += node.wholeText.replace(/ +/g, " "); 470 else 471 ret += node.wholeText; 472 } 473 else if(!node.wholeText) 474 ret += node.nodeValue; 475 }, false, true); 476 477 return ret; 478 }($element.get(0))); 479 }; 480 481 /** 482 * Converts BBCode to HTML 483 * 484 * @param {String} text 485 * @param {Bool} isFragment 486 * @return {String} HTML 487 * @memberOf jQuery.sceditorBBCodePlugin.prototype 488 */ 489 base.getTextHandler = function(text, isFragment) { 490 491 var oldText, replaceBBCodeFunc, 492 bbcodeRegex = /\[([^\[\s=]+)(?:([^\[]+))?\]((?:[\s\S](?!\[\1))*?)\[\/(\1)\]/g, 493 atribsRegex = /(\S+)=((?:(?:(["'])(?:\\\3|[^\3])*?\3))|(?:[^'"\s]+))/g; 494 495 replaceBBCodeFunc = function(str, bbcode, attrs, content) 496 { 497 var attrsMap = {}, 498 matches; 499 500 if(attrs) 501 { 502 attrs = $.trim(attrs); 503 504 // if only one attribute then remove the = from the start and strip any quotes 505 if((attrs.charAt(0) === "=" && (attrs.split("=").length - 1) <= 1) || bbcode === 'url') 506 attrsMap.defaultattr = base.stripQuotes(attrs.substr(1)); 507 else 508 { 509 if(attrs.charAt(0) === "=") 510 attrs = "defaultattr" + attrs; 511 512 while((matches = atribsRegex.exec(attrs))) 513 attrsMap[matches[1].toLowerCase()] = base.stripQuotes(matches[2]); 514 } 515 } 516 517 if(!base.bbcodes[bbcode]) 518 return str; 519 520 if($.isFunction(base.bbcodes[bbcode].html)) 521 return base.bbcodes[bbcode].html.call(base, bbcode, attrsMap, content); 522 else 523 return formatString(base.bbcodes[bbcode].html, content); 524 }; 525 526 text = text.replace(/&/g, "&") 527 .replace(/</g, "<") 528 .replace(/>/g, ">") 529 .replace(/\r/g, "") 530 .replace(/(\[\/?(?:left|center|right|justify|align|rtl|ltr)\])\n/g, "$1") 531 .replace(/\n/g, "<br />"); 532 533 while(text !== oldText) 534 { 535 oldText = text; 536 text = text.replace(bbcodeRegex, replaceBBCodeFunc); 537 } 538 539 // As hr is the only bbcode not to have a start and end tag it's 540 // just being replace here instead of adding support for it above. 541 text = text.replace(/\[hr\]/gi, "<hr>") 542 .replace(/\[\*\]/gi, "<li>"); 543 544 // replace multi-spaces which are not inside tags with a non-breaking space 545 // to preserve them. Otherwise they will just be converted to 1! 546 text = text.replace(/ {2}(?=([^<\>]*?<|[^<\>]*?$))/g, " "); 547 548 return wrapInDivs(text, isFragment); 549 }; 550 551 /** 552 * Wraps divs around inline HTML. Needed for IE 553 * 554 * @param string html 555 * @return string HTML 556 * @private 557 */ 558 wrapInDivs = function(html, excludeFirstLast) 559 { 560 var d = document, 561 inlineFrag = d.createDocumentFragment(), 562 outputDiv = d.createElement('div'), 563 tmpDiv = d.createElement('div'), 564 div, node, next, nodeName; 565 566 $(tmpDiv).hide().appendTo(d.body); 567 tmpDiv.innerHTML = html; 568 569 node = tmpDiv.firstChild; 570 while(node) 571 { 572 next = node.nextSibling; 573 nodeName = node.nodeName.toLowerCase(); 574 575 if((node.nodeType === 1 && !$.sceditor.dom.isInline(node)) || nodeName === "br") 576 { 577 if(inlineFrag.childNodes.length > 0 || nodeName === "br") 578 { 579 div = d.createElement('div'); 580 div.appendChild(inlineFrag); 581 582 // Putting BR in a div in IE9 causes it to do a double line break, 583 // as much as I hate browser UA sniffing, to do feature detection would 584 // be more code than it's worth for this specific bug. 585 if(nodeName === "br" && (!$.sceditor.ie || $.sceditor.ie < 9)) 586 div.appendChild(d.createElement('br')); 587 588 outputDiv.appendChild(div); 589 inlineFrag = d.createDocumentFragment(); 590 } 591 592 if(nodeName !== "br") 593 outputDiv.appendChild(node); 594 } 595 else 596 inlineFrag.appendChild(node); 597 598 node = next; 599 } 600 601 if(inlineFrag.childNodes.length > 0) 602 { 603 div = d.createElement('div'); 604 div.appendChild(inlineFrag); 605 outputDiv.appendChild(div); 606 } 607 608 // needed for paste, the first shouldn't be wrapped in a div 609 if(excludeFirstLast) 610 { 611 node = outputDiv.firstChild; 612 if(node && node.nodeName.toLowerCase() === "div") 613 { 614 while((next = node.firstChild)) 615 outputDiv.insertBefore(next, node); 616 617 if($.sceditor.ie >= 9) 618 outputDiv.insertBefore(d.createElement('br'), node); 619 620 outputDiv.removeChild(node); 621 } 622 623 node = outputDiv.lastChild; 624 if(node && node.nodeName.toLowerCase() === "div") 625 { 626 while((next = node.firstChild)) 627 outputDiv.insertBefore(next, node); 628 629 if($.sceditor.ie >= 9) 630 outputDiv.insertBefore(d.createElement('br'), node); 631 632 outputDiv.removeChild(node); 633 } 634 } 635 636 $(tmpDiv).remove(); 637 return outputDiv.innerHTML; 638 }; 639 640 init(); 641 }; 642 643 $.sceditorBBCodePlugin.bbcodes = { 644 // START_COMMAND: Bold 645 b: { 646 tags: { 647 b: null, 648 strong: null 649 }, 650 styles: { 651 // 401 is for FF 3.5 652 "font-weight": ["bold", "bolder", "401", "700", "800", "900"] 653 }, 654 format: "[b]{0}[/b]", 655 html: '<strong>{0}</strong>' 656 }, 657 // END_COMMAND 658 659 // START_COMMAND: Italic 660 i: { 661 tags: { 662 i: null, 663 em: null 664 }, 665 styles: { 666 "font-style": ["italic", "oblique"] 667 }, 668 format: "[i]{0}[/i]", 669 html: '<em>{0}</em>' 670 }, 671 // END_COMMAND 672 673 // START_COMMAND: Underline 674 u: { 675 tags: { 676 u: null 677 }, 678 styles: { 679 "text-decoration": ["underline"] 680 }, 681 format: "[u]{0}[/u]", 682 html: '<u>{0}</u>' 683 }, 684 // END_COMMAND 685 686 // START_COMMAND: Strikethrough 687 s: { 688 tags: { 689 s: null, 690 strike: null 691 }, 692 styles: { 693 "text-decoration": ["line-through"] 694 }, 695 format: "[s]{0}[/s]", 696 html: '<s>{0}</s>' 697 }, 698 // END_COMMAND 699 700 // START_COMMAND: Subscript 701 sub: { 702 tags: { 703 sub: null 704 }, 705 format: "[sub]{0}[/sub]", 706 html: '<sub>{0}</sub>' 707 }, 708 // END_COMMAND 709 710 // START_COMMAND: Superscript 711 sup: { 712 tags: { 713 sup: null 714 }, 715 format: "[sup]{0}[/sup]", 716 html: '<sup>{0}</sup>' 717 }, 718 // END_COMMAND 719 720 // START_COMMAND: Font 721 font: { 722 tags: { 723 font: { 724 face: null 725 } 726 }, 727 styles: { 728 "font-family": null 729 }, 730 format: function(element, content) { 731 if(element[0].nodeName.toLowerCase() === "font" && element.attr('face')) 732 return '[font=' + this.stripQuotes(element.attr('face')) + ']' + content + '[/font]'; 733 734 return '[font=' + this.stripQuotes(element.css('font-family')) + ']' + content + '[/font]'; 735 }, 736 html: function(element, attrs, content) { 737 return '<font face="' + attrs.defaultattr + '">' + content + '</font>'; 738 } 739 }, 740 // END_COMMAND 741 742 // START_COMMAND: Size 743 size: { 744 tags: { 745 font: { 746 size: null 747 } 748 }, 749 styles: { 750 "font-size": null 751 }, 752 format: function(element, content) { 753 var fontSize = element.css('fontSize'), 754 size = 1; 755 756 // Most browsers return px value but IE returns 1-7 757 if(fontSize.indexOf("px") > -1) { 758 // convert size to an int 759 fontSize = fontSize.replace("px", "") - 0; 760 761 if(fontSize > 12) 762 size = 2; 763 if(fontSize > 15) 764 size = 3; 765 if(fontSize > 17) 766 size = 4; 767 if(fontSize > 23) 768 size = 5; 769 if(fontSize > 31) 770 size = 6; 771 if(fontSize > 47) 772 size = 7; 773 } 774 else 775 size = fontSize; 776 777 return '[size=' + size + ']' + content + '[/size]'; 778 }, 779 html: function(element, attrs, content) { 780 return '<font size="' + attrs.defaultattr + '">' + content + '</font>'; 781 } 782 }, 783 // END_COMMAND 784 785 // START_COMMAND: Color 786 color: { 787 tags: { 788 font: { 789 color: null 790 } 791 }, 792 styles: { 793 color: null 794 }, 795 format: function(element, content) { 796 /** 797 * Converts CSS rgb value into hex 798 * @private 799 * @return string Hex color 800 */ 801 var rgbToHex = function(rgbStr) { 802 var m; 803 804 function toHex(n) { 805 n = parseInt(n,10); 806 if(isNaN(n)) 807 return "00"; 808 n = Math.max(0,Math.min(n,255)).toString(16); 809 810 return n.length<2 ? '0'+n : n; 811 } 812 813 // rgb(n,n,n); 814 if((m = rgbStr.match(/rgb\((\d+),\s*?(\d+),\s*?(\d+)\)/i))) 815 return '#' + toHex(m[1]) + toHex(m[2]-0) + toHex(m[3]-0); 816 817 // expand shorthand 818 if((m = rgbStr.match(/#([0-f])([0-f])([0-f])\s*?$/i))) 819 return '#' + m[1] + m[1] + m[2] + m[2] + m[3] + m[3]; 820 821 return rgbStr; 822 }; 823 824 var color = element.css('color'); 825 826 if(element[0].nodeName.toLowerCase() === "font" && element.attr('color')) 827 color = element.attr('color'); 828 829 color = rgbToHex(color); 830 831 return '[color=' + color + ']' + content + '[/color]'; 832 }, 833 html: function(element, attrs, content) { 834 return '<font color="' + attrs.defaultattr + '">' + content + '</font>'; 835 } 836 }, 837 // END_COMMAND 838 839 // START_COMMAND: Lists 840 ul: { 841 tags: { 842 ul: null 843 }, 844 isBlock: true, 845 format: "[ul]{0}[/ul]", 846 html: '<ul>{0}</ul>' 847 }, 848 list: { 849 html: '<ul>{0}</ul>' 850 }, 851 ol: { 852 tags: { 853 ol: null 854 }, 855 isBlock: true, 856 format: "[ol]{0}[/ol]", 857 html: '<ol>{0}</ol>' 858 }, 859 li: { 860 tags: { 861 li: null 862 }, 863 format: "[li]{0}[/li]", 864 html: '<li>{0}</li>' 865 }, 866 "*": { 867 html: '<li>{0}</li>' 868 }, 869 // END_COMMAND 870 871 // START_COMMAND: Table 872 table: { 873 tags: { 874 table: null 875 }, 876 format: "[table]{0}[/table]", 877 html: '<table>{0}</table>' 878 }, 879 tr: { 880 tags: { 881 tr: null 882 }, 883 format: "[tr]{0}[/tr]", 884 html: '<tr>{0}</tr>' 885 }, 886 th: { 887 tags: { 888 th: null 889 }, 890 isBlock: true, 891 format: "[th]{0}[/th]", 892 html: '<th>{0}</th>' 893 }, 894 td: { 895 tags: { 896 td: null 897 }, 898 isBlock: true, 899 format: "[td]{0}[/td]", 900 html: '<td>{0}<br class="sceditor-ignore" /></td>' 901 }, 902 // END_COMMAND 903 904 // START_COMMAND: Emoticons 905 emoticon: { 906 allowsEmpty: true, 907 tags: { 908 img: { 909 src: null, 910 "data-sceditor-emoticon": null 911 } 912 }, 913 format: function(element, content) { 914 return element.attr('data-sceditor-emoticon') + content; 915 }, 916 html: '{0}' 917 }, 918 // END_COMMAND 919 920 // START_COMMAND: Horizontal Rule 921 horizontalrule: { 922 allowsEmpty: true, 923 tags: { 924 hr: null 925 }, 926 format: "[hr]{0}", 927 html: "<hr />" 928 }, 929 // END_COMMAND 930 931 // START_COMMAND: Image 932 img: { 933 allowsEmpty: true, 934 tags: { 935 img: { 936 src: null 937 } 938 }, 939 format: function(element, content) { 940 // check if this is an emoticon image 941 if(typeof element.attr('data-sceditor-emoticon') !== "undefined") 942 return content; 943 944 var attribs = "=" + $(element).width() + "x" + $(element).height(); 945 946 return '[img' + attribs + ']' + element.attr('src') + '[/img]'; 947 }, 948 html: function(element, attrs, content) { 949 var attribs = "", parts; 950 951 // handle [img width=340 height=240]url[/img] 952 if(typeof attrs.width !== "undefined") 953 attribs += ' width="' + attrs.width + '"'; 954 if(typeof attrs.height !== "undefined") 955 attribs += ' height="' + attrs.height + '"'; 956 957 // handle [img=340x240]url[/img] 958 if(typeof attrs.defaultattr !== "undefined") { 959 parts = attrs.defaultattr.split(/x/i); 960 961 attribs = ' width="' + parts[0] + '"' + 962 ' height="' + (parts.length === 2 ? parts[1] : parts[0]) + '"'; 963 } 964 965 return '<img ' + attribs + ' src="' + content + '" />'; 966 } 967 }, 968 // END_COMMAND 969 970 // START_COMMAND: URL 971 url: { 972 allowsEmpty: true, 973 tags: { 974 a: { 975 href: null 976 } 977 }, 978 format: function(element, content) { 979 // make sure this link is not an e-mail, if it is return e-mail BBCode 980 if(element.attr('href').substr(0, 7) === 'mailto:') 981 return '[email=' + element.attr('href').substr(7) + ']' + content + '[/email]'; 982 983 return '[url=' + decodeURI(element.attr('href')) + ']' + content + '[/url]'; 984 }, 985 html: function(element, attrs, content) { 986 if(typeof attrs.defaultattr === "undefined" || attrs.defaultattr.length === 0) 987 attrs.defaultattr = content; 988 989 return '<a href="' + encodeURI(attrs.defaultattr) + '">' + content + '</a>'; 990 } 991 }, 992 // END_COMMAND 993 994 // START_COMMAND: E-mail 995 email: { 996 html: function(element, attrs, content) { 997 if(typeof attrs.defaultattr === "undefined") 998 attrs.defaultattr = content; 999 1000 return '<a href="mailto:' + attrs.defaultattr + '">' + content + '</a>'; 1001 } 1002 }, 1003 // END_COMMAND 1004 1005 // START_COMMAND: Quote 1006 quote: { 1007 tags: { 1008 blockquote: null 1009 }, 1010 isBlock: true, 1011 format: function(element, content) { 1012 var author, 1013 attr = '', 1014 $elm = $(element); 1015 1016 if($elm.children("cite:first").length === 1 || $elm.data("author")) { 1017 author = $(element).children("cite:first").text() || $elm.data("author"); 1018 1019 1020 $elm.data("author", author) 1021 $(element).children("cite:first").remove(); 1022 1023 content = ''; 1024 content = this.elementToBbcode($(element)); 1025 attr = '=' + author; 1026 } 1027 1028 return '[quote' + attr + ']' + content + '[/quote]'; 1029 }, 1030 html: function(element, attrs, content) { 1031 if(typeof attrs.defaultattr !== "undefined") 1032 content = '<cite>' + attrs.defaultattr + '</cite>' + content; 1033 1034 return '<blockquote>' + content + '</blockquote>'; 1035 } 1036 }, 1037 // END_COMMAND 1038 1039 // START_COMMAND: Code 1040 code: { 1041 tags: { 1042 code: null 1043 }, 1044 isBlock: true, 1045 format: "[code]{0}[/code]", 1046 html: '<code>{0}</code>' 1047 }, 1048 // END_COMMAND 1049 1050 1051 // START_COMMAND: Left 1052 left: { 1053 styles: { 1054 "text-align": ["left", "-webkit-left", "-moz-left", "-khtml-left"] 1055 }, 1056 isBlock: true, 1057 format: "[left]{0}[/left]", 1058 html: '<div align="left">{0}</div>' 1059 }, 1060 // END_COMMAND 1061 1062 // START_COMMAND: Centre 1063 center: { 1064 styles: { 1065 "text-align": ["center", "-webkit-center", "-moz-center", "-khtml-center"] 1066 }, 1067 isBlock: true, 1068 format: "[center]{0}[/center]", 1069 html: '<div align="center">{0}</div>' 1070 }, 1071 // END_COMMAND 1072 1073 // START_COMMAND: Right 1074 right: { 1075 styles: { 1076 "text-align": ["right", "-webkit-right", "-moz-right", "-khtml-right"] 1077 }, 1078 isBlock: true, 1079 format: "[right]{0}[/right]", 1080 html: '<div align="right">{0}</div>' 1081 }, 1082 // END_COMMAND 1083 1084 // START_COMMAND: Justify 1085 justify: { 1086 styles: { 1087 "text-align": ["justify", "-webkit-justify", "-moz-justify", "-khtml-justify"] 1088 }, 1089 isBlock: true, 1090 format: "[justify]{0}[/justify]", 1091 html: '<div align="justify">{0}</div>' 1092 }, 1093 // END_COMMAND 1094 1095 // START_COMMAND: YouTube 1096 youtube: { 1097 allowsEmpty: true, 1098 tags: { 1099 iframe: { 1100 'data-youtube-id': null 1101 } 1102 }, 1103 format: function(element, content) { 1104 if(!element.attr('data-youtube-id')) 1105 return content; 1106 1107 return '[youtube]' + element.attr('data-youtube-id') + '[/youtube]'; 1108 }, 1109 html: '<iframe width="560" height="315" src="http://www.youtube.com/embed/{0}?wmode=opaque' + 1110 '" data-youtube-id="{0}" frameborder="0" allowfullscreen></iframe>' 1111 }, 1112 // END_COMMAND 1113 1114 1115 // START_COMMAND: Rtl 1116 rtl: { 1117 styles: { 1118 "direction": ["rtl"] 1119 }, 1120 format: "[rtl]{0}[/rtl]", 1121 html: '<div style="direction: rtl">{0}</div>' 1122 }, 1123 // END_COMMAND 1124 1125 // START_COMMAND: Ltr 1126 ltr: { 1127 styles: { 1128 "direction": ["ltr"] 1129 }, 1130 format: "[ltr]{0}[/ltr]", 1131 html: '<div style="direction: ltr">{0}</div>' 1132 }, 1133 // END_COMMAND 1134 1135 1136 // this is here so that commands above can be removed 1137 // without having to remove the , after the last one. 1138 // Needed for IE. 1139 ignore: {} 1140 }; 1141 1142 /** 1143 * Static BBCode helper class 1144 * @class command 1145 * @name jQuery.sceditorBBCodePlugin.bbcode 1146 */ 1147 $.sceditorBBCodePlugin.bbcode = 1148 /** @lends jQuery.sceditorBBCodePlugin.bbcode */ 1149 { 1150 /** 1151 * Gets a BBCode 1152 * 1153 * @param {String} name 1154 * @return {Object|null} 1155 * @since v1.3.5 1156 */ 1157 get: function(name) { 1158 return $.sceditorBBCodePlugin.bbcodes[name] || null; 1159 }, 1160 1161 /** 1162 * <p>Adds a BBCode to the parser or updates an exisiting 1163 * BBCode if a BBCode with the specified name already exists.</p> 1164 * 1165 * @param {String} name 1166 * @param {Object} bbcode 1167 * @return {this|false} Returns false if name or bbcode is false 1168 * @since v1.3.5 1169 */ 1170 set: function(name, bbcode) { 1171 if(!name || !bbcode) 1172 return false; 1173 1174 // merge any existing command properties 1175 bbcode = $.extend($.sceditorBBCodePlugin.bbcodes[name] || {}, bbcode); 1176 1177 bbcode.remove = function() { $.sceditorBBCodePlugin.bbcode.remove(name); }; 1178 1179 $.sceditorBBCodePlugin.bbcodes[name] = bbcode; 1180 return this; 1181 }, 1182 1183 /** 1184 * Removes a BBCode 1185 * 1186 * @param {String} name 1187 * @return {this} 1188 * @since v1.3.5 1189 */ 1190 remove: function(name) { 1191 if($.sceditorBBCodePlugin.bbcodes[name]) 1192 delete $.sceditorBBCodePlugin.bbcodes[name]; 1193 1194 return this; 1195 } 1196 }; 1197 1198 /** 1199 * Checks if a command with the specified name exists 1200 * 1201 * @param string name 1202 * @return bool 1203 * @deprecated Since v1.3.5 1204 * @memberOf jQuery.sceditorBBCodePlugin 1205 */ 1206 $.sceditorBBCodePlugin.commandExists = function(name) { 1207 return !!$.sceditorBBCodePlugin.bbcode.get(name); 1208 }; 1209 1210 /** 1211 * Adds/updates a BBCode. 1212 * 1213 * @param String name The BBCode name 1214 * @param Object tags Any html tags this bbcode applies to, i.e. strong for [b] 1215 * @param Object styles Any style properties this applies to, i.e. font-weight for [b] 1216 * @param String|Function format Function or string to convert the element into BBCode 1217 * @param String|Function html String or function to format the BBCode back into HTML. 1218 * @param bool allowsEmpty If this BBCodes is allowed to be empty, e.g. [b][/b] 1219 * @return Bool 1220 * @deprecated Since v1.3.5 1221 * @memberOf jQuery.sceditorBBCodePlugin 1222 */ 1223 $.sceditorBBCodePlugin.setCommand = function(name, tags, styles, format, html, allowsEmpty, isBlock) { 1224 return $.sceditorBBCodePlugin.bbcode.set(name, 1225 { 1226 tags: tags || {}, 1227 styles: styles || {}, 1228 allowsEmpty: allowsEmpty, 1229 isBlock: isBlock, 1230 format: format, 1231 html: html 1232 }); 1233 }; 1234 1235 $.fn.sceditorBBCodePlugin = function(options) { 1236 if((!options || !options.runWithoutWysiwygSupport) && !$.sceditor.isWysiwygSupported()) 1237 return; 1238 1239 return this.each(function() { 1240 (new $.sceditorBBCodePlugin(this, options)); 1241 }); 1242 }; 1243 })(jQuery); 1244