jquery.markitup.min.js (18300B)
1 // ---------------------------------------------------------------------------- 2 // markItUp! Universal MarkUp Engine, JQuery plugin 3 // v 1.1.x 4 // Dual licensed under the MIT and GPL licenses. 5 // ---------------------------------------------------------------------------- 6 // Copyright (C) 2007-2011 Jay Salvat 7 // http://markitup.jaysalvat.com/ 8 // ---------------------------------------------------------------------------- 9 // Permission is hereby granted, free of charge, to any person obtaining a copy 10 // of this software and associated documentation files (the "Software"), to deal 11 // in the Software without restriction, including without limitation the rights 12 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 // copies of the Software, and to permit persons to whom the Software is 14 // furnished to do so, subject to the following conditions: 15 // 16 // The above copyright notice and this permission notice shall be included in 17 // all copies or substantial portions of the Software. 18 // 19 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 // THE SOFTWARE. 26 // ---------------------------------------------------------------------------- 27 (function($) { 28 $.fn.markItUp = function(settings, extraSettings) { 29 var options, ctrlKey, shiftKey, altKey; 30 ctrlKey = shiftKey = altKey = false; 31 32 options = { id: '', 33 nameSpace: '', 34 root: '', 35 previewInWindow: '', // 'width=800, height=600, resizable=yes, scrollbars=yes' 36 previewAutoRefresh: true, 37 previewPosition: 'after', 38 previewTemplatePath: '~/templates/preview.html', 39 previewParser: false, 40 previewParserPath: '', 41 previewParserVar: 'data', 42 resizeHandle: true, 43 beforeInsert: '', 44 afterInsert: '', 45 onEnter: {}, 46 onShiftEnter: {}, 47 onCtrlEnter: {}, 48 onTab: {}, 49 markupSet: [ { /* set */ } ] 50 }; 51 $.extend(options, settings, extraSettings); 52 53 // compute markItUp! path 54 if (!options.root) { 55 $('script').each(function(a, tag) { 56 miuScript = $(tag).get(0).src.match(/(.*)jquery\.markitup(\.pack)?\.js$/); 57 if (miuScript !== null) { 58 options.root = miuScript[1]; 59 } 60 }); 61 } 62 63 return this.each(function() { 64 var $$, textarea, levels, scrollPosition, caretPosition, caretOffset, 65 clicked, hash, header, footer, previewWindow, template, iFrame, abort; 66 $$ = $(this); 67 textarea = this; 68 levels = []; 69 abort = false; 70 scrollPosition = caretPosition = 0; 71 caretOffset = -1; 72 73 options.previewParserPath = localize(options.previewParserPath); 74 options.previewTemplatePath = localize(options.previewTemplatePath); 75 76 // apply the computed path to ~/ 77 function localize(data, inText) { 78 if (inText) { 79 return data.replace(/("|')~\//g, "$1"+options.root); 80 } 81 return data.replace(/^~\//, options.root); 82 } 83 84 // init and build editor 85 function init() { 86 id = ''; nameSpace = ''; 87 if (options.id) { 88 id = 'id="'+options.id+'"'; 89 } else if ($$.attr("id")) { 90 id = 'id="markItUp'+($$.attr("id").substr(0, 1).toUpperCase())+($$.attr("id").substr(1))+'"'; 91 92 } 93 if (options.nameSpace) { 94 nameSpace = 'class="'+options.nameSpace+'"'; 95 } 96 $$.wrap('<div '+nameSpace+'></div>'); 97 $$.wrap('<div '+id+' class="markItUp"></div>'); 98 $$.wrap('<div class="markItUpContainer"></div>'); 99 $$.addClass("markItUpEditor"); 100 101 // add the header before the textarea 102 header = $('<div class="markItUpHeader"></div>').insertBefore($$); 103 $(dropMenus(options.markupSet)).appendTo(header); 104 105 // add the footer after the textarea 106 footer = $('<div class="markItUpFooter"></div>').insertAfter($$); 107 108 // add the resize handle after textarea 109 if (options.resizeHandle === true && $.browser.safari !== true) { 110 resizeHandle = $('<div class="markItUpResizeHandle"></div>') 111 .insertAfter($$) 112 .bind("mousedown", function(e) { 113 var h = $$.height(), y = e.clientY, mouseMove, mouseUp; 114 mouseMove = function(e) { 115 $$.css("height", Math.max(20, e.clientY+h-y)+"px"); 116 return false; 117 }; 118 mouseUp = function(e) { 119 $("html").unbind("mousemove", mouseMove).unbind("mouseup", mouseUp); 120 return false; 121 }; 122 $("html").bind("mousemove", mouseMove).bind("mouseup", mouseUp); 123 }); 124 footer.append(resizeHandle); 125 } 126 127 // listen key events 128 $$.keydown(keyPressed).keyup(keyPressed); 129 130 // bind an event to catch external calls 131 $$.bind("insertion", function(e, settings) { 132 if (settings.target !== false) { 133 get(); 134 } 135 if (textarea === $.markItUp.focused) { 136 markup(settings); 137 } 138 }); 139 140 // remember the last focus 141 $$.focus(function() { 142 $.markItUp.focused = this; 143 }); 144 } 145 146 // recursively build header with dropMenus from markupset 147 function dropMenus(markupSet) { 148 var ul = $('<ul></ul>'), i = 0; 149 $('li:hover > ul', ul).css('display', 'block'); 150 $.each(markupSet, function() { 151 var button = this, t = '', title, li, j; 152 title = (button.key) ? (button.name||'')+' [Ctrl+'+button.key+']' : (button.name||''); 153 key = (button.key) ? 'accesskey="'+button.key+'"' : ''; 154 if (button.separator) { 155 li = $('<li class="markItUpSeparator">'+(button.separator||'')+'</li>').appendTo(ul); 156 } else { 157 i++; 158 for (j = levels.length -1; j >= 0; j--) { 159 t += levels[j]+"-"; 160 } 161 li = $('<li class="markItUpButton markItUpButton'+t+(i)+' '+(button.className||'')+'"><a href="" '+key+' title="'+title+'">'+(button.name||'')+'</a></li>') 162 .bind("contextmenu", function() { // prevent contextmenu on mac and allow ctrl+click 163 return false; 164 }).click(function() { 165 return false; 166 }).bind("focusin", function(){ 167 $$.focus(); 168 }).mouseup(function() { 169 if (button.call) { 170 eval(button.call)(); 171 } 172 setTimeout(function() { markup(button) },1); 173 return false; 174 }).hover(function() { 175 $('> ul', this).show(); 176 $(document).one('click', function() { // close dropmenu if click outside 177 $('ul ul', header).hide(); 178 } 179 ); 180 }, function() { 181 $('> ul', this).hide(); 182 } 183 ).appendTo(ul); 184 if (button.dropMenu) { 185 levels.push(i); 186 $(li).addClass('markItUpDropMenu').append(dropMenus(button.dropMenu)); 187 } 188 } 189 }); 190 levels.pop(); 191 return ul; 192 } 193 194 // markItUp! markups 195 function magicMarkups(string) { 196 if (string) { 197 string = string.toString(); 198 string = string.replace(/\(\!\(([\s\S]*?)\)\!\)/g, 199 function(x, a) { 200 var b = a.split('|!|'); 201 if (altKey === true) { 202 return (b[1] !== undefined) ? b[1] : b[0]; 203 } else { 204 return (b[1] === undefined) ? "" : b[0]; 205 } 206 } 207 ); 208 // [![prompt]!], [![prompt:!:value]!] 209 string = string.replace(/\[\!\[([\s\S]*?)\]\!\]/g, 210 function(x, a) { 211 var b = a.split(':!:'); 212 if (abort === true) { 213 return false; 214 } 215 value = prompt(b[0], (b[1]) ? b[1] : ''); 216 if (value === null) { 217 abort = true; 218 } 219 return value; 220 } 221 ); 222 return string; 223 } 224 return ""; 225 } 226 227 // prepare action 228 function prepare(action) { 229 if ($.isFunction(action)) { 230 action = action(hash); 231 } 232 return magicMarkups(action); 233 } 234 235 // build block to insert 236 function build(string) { 237 var openWith = prepare(clicked.openWith); 238 var placeHolder = prepare(clicked.placeHolder); 239 var replaceWith = prepare(clicked.replaceWith); 240 var closeWith = prepare(clicked.closeWith); 241 var openBlockWith = prepare(clicked.openBlockWith); 242 var closeBlockWith = prepare(clicked.closeBlockWith); 243 var multiline = clicked.multiline; 244 245 if (replaceWith !== "") { 246 block = openWith + replaceWith + closeWith; 247 } else if (selection === '' && placeHolder !== '') { 248 block = openWith + placeHolder + closeWith; 249 } else { 250 string = string || selection; 251 252 var lines = selection.split(/\r?\n/), blocks = []; 253 254 for (var l=0; l < lines.length; l++) { 255 line = lines[l]; 256 var trailingSpaces; 257 if (trailingSpaces = line.match(/ *$/)) { 258 blocks.push(openWith + line.replace(/ *$/g, '') + closeWith + trailingSpaces); 259 } else { 260 blocks.push(openWith + line + closeWith); 261 } 262 } 263 264 block = blocks.join("\n"); 265 } 266 267 block = openBlockWith + block + closeBlockWith; 268 269 return { block:block, 270 openWith:openWith, 271 replaceWith:replaceWith, 272 placeHolder:placeHolder, 273 closeWith:closeWith 274 }; 275 } 276 277 // define markup to insert 278 function markup(button) { 279 var len, j, n, i; 280 hash = clicked = button; 281 get(); 282 $.extend(hash, { line:"", 283 root:options.root, 284 textarea:textarea, 285 selection:(selection||''), 286 caretPosition:caretPosition, 287 ctrlKey:ctrlKey, 288 shiftKey:shiftKey, 289 altKey:altKey 290 } 291 ); 292 // callbacks before insertion 293 prepare(options.beforeInsert); 294 prepare(clicked.beforeInsert); 295 if ((ctrlKey === true && shiftKey === true) || button.multiline === true) { 296 prepare(clicked.beforeMultiInsert); 297 } 298 $.extend(hash, { line:1 }); 299 300 if ((ctrlKey === true && shiftKey === true)) { 301 lines = selection.split(/\r?\n/); 302 for (j = 0, n = lines.length, i = 0; i < n; i++) { 303 if ($.trim(lines[i]) !== '') { 304 $.extend(hash, { line:++j, selection:lines[i] } ); 305 lines[i] = build(lines[i]).block; 306 } else { 307 lines[i] = ""; 308 } 309 } 310 string = { block:lines.join('\n')}; 311 start = caretPosition; 312 len = string.block.length + (($.browser.opera) ? n-1 : 0); 313 } else if (ctrlKey === true) { 314 string = build(selection); 315 start = caretPosition + string.openWith.length; 316 len = string.block.length - string.openWith.length - string.closeWith.length; 317 len = len - (string.block.match(/ $/) ? 1 : 0); 318 len -= fixIeBug(string.block); 319 } else if (shiftKey === true) { 320 string = build(selection); 321 start = caretPosition; 322 len = string.block.length; 323 len -= fixIeBug(string.block); 324 } else { 325 string = build(selection); 326 start = caretPosition + string.block.length ; 327 len = 0; 328 start -= fixIeBug(string.block); 329 } 330 if ((selection === '' && string.replaceWith === '')) { 331 caretOffset += fixOperaBug(string.block); 332 333 start = caretPosition + string.openWith.length; 334 len = string.block.length - string.openWith.length - string.closeWith.length; 335 336 caretOffset = $$.val().substring(caretPosition, $$.val().length).length; 337 caretOffset -= fixOperaBug($$.val().substring(0, caretPosition)); 338 } 339 $.extend(hash, { caretPosition:caretPosition, scrollPosition:scrollPosition } ); 340 341 if (string.block !== selection && abort === false) { 342 insert(string.block); 343 set(start, len); 344 } else { 345 caretOffset = -1; 346 } 347 get(); 348 349 $.extend(hash, { line:'', selection:selection }); 350 351 // callbacks after insertion 352 if ((ctrlKey === true && shiftKey === true) || button.multiline === true) { 353 prepare(clicked.afterMultiInsert); 354 } 355 prepare(clicked.afterInsert); 356 prepare(options.afterInsert); 357 358 // refresh preview if opened 359 if (previewWindow && options.previewAutoRefresh) { 360 refreshPreview(); 361 } 362 363 // reinit keyevent 364 shiftKey = altKey = ctrlKey = abort = false; 365 } 366 367 // Substract linefeed in Opera 368 function fixOperaBug(string) { 369 if ($.browser.opera) { 370 return string.length - string.replace(/\n*/g, '').length; 371 } 372 return 0; 373 } 374 // Substract linefeed in IE 375 function fixIeBug(string) { 376 if ($.browser.msie) { 377 return string.length - string.replace(/\r*/g, '').length; 378 } 379 return 0; 380 } 381 382 // add markup 383 function insert(block) { 384 if (document.selection) { 385 var newSelection = document.selection.createRange(); 386 newSelection.text = block; 387 } else { 388 textarea.value = textarea.value.substring(0, caretPosition) + block + textarea.value.substring(caretPosition + selection.length, textarea.value.length); 389 } 390 } 391 392 // set a selection 393 function set(start, len) { 394 if (textarea.createTextRange){ 395 // quick fix to make it work on Opera 9.5 396 if ($.browser.opera && $.browser.version >= 9.5 && len == 0) { 397 return false; 398 } 399 range = textarea.createTextRange(); 400 range.collapse(true); 401 range.moveStart('character', start); 402 range.moveEnd('character', len); 403 range.select(); 404 } else if (textarea.setSelectionRange ){ 405 textarea.setSelectionRange(start, start + len); 406 } 407 textarea.scrollTop = scrollPosition; 408 textarea.focus(); 409 } 410 411 // get the selection 412 function get() { 413 textarea.focus(); 414 415 scrollPosition = textarea.scrollTop; 416 if (document.selection) { 417 selection = document.selection.createRange().text; 418 if ($.browser.msie) { // ie 419 var range = document.selection.createRange(), rangeCopy = range.duplicate(); 420 rangeCopy.moveToElementText(textarea); 421 caretPosition = -1; 422 while(rangeCopy.inRange(range)) { 423 rangeCopy.moveStart('character'); 424 caretPosition ++; 425 } 426 } else { // opera 427 caretPosition = textarea.selectionStart; 428 } 429 } else { // gecko & webkit 430 caretPosition = textarea.selectionStart; 431 432 selection = textarea.value.substring(caretPosition, textarea.selectionEnd); 433 } 434 return selection; 435 } 436 437 // open preview window 438 function preview() { 439 if (!previewWindow || previewWindow.closed) { 440 if (options.previewInWindow) { 441 previewWindow = window.open('', 'preview', options.previewInWindow); 442 $(window).unload(function() { 443 previewWindow.close(); 444 }); 445 } else { 446 iFrame = $('<iframe class="markItUpPreviewFrame"></iframe>'); 447 if (options.previewPosition == 'after') { 448 iFrame.insertAfter(footer); 449 } else { 450 iFrame.insertBefore(header); 451 } 452 previewWindow = iFrame[iFrame.length - 1].contentWindow || frame[iFrame.length - 1]; 453 } 454 } else if (altKey === true) { 455 if (iFrame) { 456 iFrame.remove(); 457 } else { 458 previewWindow.close(); 459 } 460 previewWindow = iFrame = false; 461 } 462 if (!options.previewAutoRefresh) { 463 refreshPreview(); 464 } 465 if (options.previewInWindow) { 466 previewWindow.focus(); 467 } 468 } 469 470 // refresh Preview window 471 function refreshPreview() { 472 renderPreview(); 473 } 474 475 function renderPreview() { 476 var phtml; 477 if (options.previewParser && typeof options.previewParser === 'function') { 478 var data = options.previewParser( $$.val() ); 479 writeInPreview( localize(data, 1) ); 480 } else if (options.previewParserPath !== '') { 481 $.ajax({ 482 type: 'POST', 483 dataType: 'text', 484 global: false, 485 url: options.previewParserPath, 486 data: options.previewParserVar+'='+encodeURIComponent($$.val()), 487 success: function(data) { 488 writeInPreview( localize(data, 1) ); 489 } 490 }); 491 } else { 492 if (!template) { 493 $.ajax({ 494 url: options.previewTemplatePath, 495 dataType: 'text', 496 global: false, 497 success: function(data) { 498 writeInPreview( localize(data, 1).replace(/<!-- content -->/g, $$.val()) ); 499 } 500 }); 501 } 502 } 503 return false; 504 } 505 506 function writeInPreview(data) { 507 if (previewWindow.document) { 508 try { 509 sp = previewWindow.document.documentElement.scrollTop 510 } catch(e) { 511 sp = 0; 512 } 513 previewWindow.document.open(); 514 previewWindow.document.write(data); 515 previewWindow.document.close(); 516 previewWindow.document.documentElement.scrollTop = sp; 517 } 518 } 519 520 // set keys pressed 521 function keyPressed(e) { 522 shiftKey = e.shiftKey; 523 altKey = e.altKey; 524 ctrlKey = (!(e.altKey && e.ctrlKey)) ? (e.ctrlKey || e.metaKey) : false; 525 526 if (e.type === 'keydown') { 527 if (ctrlKey === true) { 528 li = $('a[accesskey="'+String.fromCharCode(e.keyCode)+'"]', header).parent('li'); 529 if (li.length !== 0) { 530 ctrlKey = false; 531 setTimeout(function() { 532 li.triggerHandler('mouseup'); 533 },1); 534 return false; 535 } 536 } 537 if (e.keyCode === 13 || e.keyCode === 10) { // Enter key 538 if (ctrlKey === true) { // Enter + Ctrl 539 ctrlKey = false; 540 markup(options.onCtrlEnter); 541 return options.onCtrlEnter.keepDefault; 542 } else if (shiftKey === true) { // Enter + Shift 543 shiftKey = false; 544 markup(options.onShiftEnter); 545 return options.onShiftEnter.keepDefault; 546 } else { // only Enter 547 markup(options.onEnter); 548 return options.onEnter.keepDefault; 549 } 550 } 551 if (e.keyCode === 9) { // Tab key 552 if (shiftKey == true || ctrlKey == true || altKey == true) { 553 return false; 554 } 555 if (caretOffset !== -1) { 556 get(); 557 caretOffset = $$.val().length - caretOffset; 558 set(caretOffset, 0); 559 caretOffset = -1; 560 return false; 561 } else { 562 markup(options.onTab); 563 return options.onTab.keepDefault; 564 } 565 } 566 } 567 } 568 569 init(); 570 }); 571 }; 572 573 $.fn.markItUpRemove = function() { 574 return this.each(function() { 575 var $$ = $(this).unbind().removeClass('markItUpEditor'); 576 $$.parent('div').parent('div.markItUp').parent('div').replaceWith($$); 577 } 578 ); 579 }; 580 581 $.markItUp = function(settings) { 582 var options = { target:false }; 583 $.extend(options, settings); 584 if (options.target) { 585 return $(options.target).each(function() { 586 $(this).focus(); 587 $(this).trigger('insertion', [options]); 588 }); 589 } else { 590 $('textarea').trigger('insertion', [options]); 591 } 592 }; 593 })(jQuery);