1 // ----------------------------------------------------------------------------
2 // markItUp! Universal MarkUp Engine, JQuery plugin
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:
16 // The above copyright notice and this permission notice shall be included in
17 // all copies or substantial portions of the Software.
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
26 // ----------------------------------------------------------------------------
28 $.fn
.markItUp = function(settings
, extraSettings
) {
29 var options
, ctrlKey
, shiftKey
, altKey
;
30 ctrlKey
= shiftKey
= altKey
= false;
35 previewInWindow
: '', // 'width=800, height=600, resizable=yes, scrollbars=yes'
36 previewAutoRefresh
: true,
37 previewPosition
: 'after',
38 previewTemplatePath
: '~/templates/preview.html',
40 previewParserPath
: '',
41 previewParserVar
: 'data',
49 markupSet
: [ { /* set */ } ]
51 $.extend(options
, settings
, extraSettings
);
53 // compute markItUp! path
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];
63 return this.each(function() {
64 var $$, textarea
, levels
, scrollPosition
, caretPosition
, caretOffset
,
65 clicked
, hash
, header
, footer
, previewWindow
, template
, iFrame
, abort
;
70 scrollPosition
= caretPosition
= 0;
73 options
.previewParserPath
= localize(options
.previewParserPath
);
74 options
.previewTemplatePath
= localize(options
.previewTemplatePath
);
76 // apply the computed path to ~/
77 function localize(data
, inText
) {
79 return data
.replace(/("|')~\//g, "$1"+options
.root
);
81 return data
.replace(/^~\//, options
.root
);
84 // init and build editor
86 id
= ''; nameSpace
= '';
88 id
= 'id="'+options
.id
+'"';
89 } else if ($$.attr("id")) {
90 id
= 'id="markItUp'+($$.attr("id").substr(0, 1).toUpperCase())+($$.attr("id").substr(1))+'"';
93 if (options
.nameSpace
) {
94 nameSpace
= 'class="'+options
.nameSpace
+'"';
96 $$.wrap('<div '+nameSpace
+'></div>');
97 $$.wrap('<div '+id
+' class="markItUp"></div>');
98 $$.wrap('<div class="markItUpContainer"></div>');
99 $$.addClass("markItUpEditor");
101 // add the header before the textarea
102 header
= $('<div class="markItUpHeader"></div>').insertBefore($$);
103 $(dropMenus(options
.markupSet
)).appendTo(header
);
105 // add the footer after the textarea
106 footer
= $('<div class="markItUpFooter"></div>').insertAfter($$);
108 // add the resize handle after textarea
109 if (options
.resizeHandle
=== true && $.browser
.safari
!== true) {
110 resizeHandle
= $('<div class="markItUpResizeHandle"></div>')
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");
118 mouseUp = function(e
) {
119 $("html").unbind("mousemove", mouseMove
).unbind("mouseup", mouseUp
);
122 $("html").bind("mousemove", mouseMove
).bind("mouseup", mouseUp
);
124 footer
.append(resizeHandle
);
128 $$.keydown(keyPressed
).keyup(keyPressed
);
130 // bind an event to catch external calls
131 $$.bind("insertion", function(e
, settings
) {
132 if (settings
.target
!== false) {
135 if (textarea
=== $.markItUp
.focused
) {
140 // remember the last focus
141 $$.focus(function() {
142 $.markItUp
.focused
= this;
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
);
158 for (j
= levels
.length
-1; j
>= 0; j
--) {
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
164 }).click(function() {
166 }).bind("focusin", function(){
168 }).mouseup(function() {
172 setTimeout(function() { markup(button
) },1);
174 }).hover(function() {
175 $('> ul', this).show();
176 $(document
).one('click', function() { // close dropmenu if click outside
177 $('ul ul', header
).hide();
181 $('> ul', this).hide();
184 if (button
.dropMenu
) {
186 $(li
).addClass('markItUpDropMenu').append(dropMenus(button
.dropMenu
));
195 function magicMarkups(string
) {
197 string
= string
.toString();
198 string
= string
.replace(/\(\!\(([\s\S]*?)\)\!\)/g,
200 var b
= a
.split('|!|');
201 if (altKey
=== true) {
202 return (b
[1] !== undefined) ? b
[1] : b
[0];
204 return (b
[1] === undefined) ? "" : b
[0];
208 // [![prompt]!], [![prompt:!:value]!]
209 string
= string
.replace(/\[\!\[([\s\S]*?)\]\!\]/g,
211 var b
= a
.split(':!:');
212 if (abort
=== true) {
215 value
= prompt(b
[0], (b
[1]) ? b
[1] : '');
216 if (value
=== null) {
228 function prepare(action
) {
229 if ($.isFunction(action
)) {
230 action
= action(hash
);
232 return magicMarkups(action
);
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
;
245 if (replaceWith
!== "") {
246 block
= openWith
+ replaceWith
+ closeWith
;
247 } else if (selection
=== '' && placeHolder
!== '') {
248 block
= openWith
+ placeHolder
+ closeWith
;
250 string
= string
|| selection
;
252 var lines
= selection
.split(/\r?\n/), blocks
= [];
254 for (var l
=0; l
< lines
.length
; l
++) {
257 if (trailingSpaces
= line
.match(/ *$/)) {
258 blocks
.push(openWith
+ line
.replace(/ *$/g
, '') + closeWith
+ trailingSpaces
);
260 blocks
.push(openWith
+ line
+ closeWith
);
264 block
= blocks
.join("\n");
267 block
= openBlockWith
+ block
+ closeBlockWith
;
269 return { block
:block
,
271 replaceWith
:replaceWith
,
272 placeHolder
:placeHolder
,
277 // define markup to insert
278 function markup(button
) {
280 hash
= clicked
= button
;
282 $.extend(hash
, { line
:"",
285 selection
:(selection
||''),
286 caretPosition
:caretPosition
,
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
);
298 $.extend(hash
, { line
:1 });
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
;
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
);
325 string
= build(selection
);
326 start
= caretPosition
+ string
.block
.length
;
328 start
-= fixIeBug(string
.block
);
330 if ((selection
=== '' && string
.replaceWith
=== '')) {
331 caretOffset
+= fixOperaBug(string
.block
);
333 start
= caretPosition
+ string
.openWith
.length
;
334 len
= string
.block
.length
- string
.openWith
.length
- string
.closeWith
.length
;
336 caretOffset
= $$.val().substring(caretPosition
, $$.val().length
).length
;
337 caretOffset
-= fixOperaBug($$.val().substring(0, caretPosition
));
339 $.extend(hash
, { caretPosition
:caretPosition
, scrollPosition
:scrollPosition
} );
341 if (string
.block
!== selection
&& abort
=== false) {
342 insert(string
.block
);
349 $.extend(hash
, { line
:'', selection
:selection
});
351 // callbacks after insertion
352 if ((ctrlKey
=== true && shiftKey
=== true) || button
.multiline
=== true) {
353 prepare(clicked
.afterMultiInsert
);
355 prepare(clicked
.afterInsert
);
356 prepare(options
.afterInsert
);
358 // refresh preview if opened
359 if (previewWindow
&& options
.previewAutoRefresh
) {
364 shiftKey
= altKey
= ctrlKey
= abort
= false;
367 // Substract linefeed in Opera
368 function fixOperaBug(string
) {
369 if ($.browser
.opera
) {
370 return string
.length
- string
.replace(/\n*/g, '').length
;
374 // Substract linefeed in IE
375 function fixIeBug(string
) {
376 if ($.browser
.msie
) {
377 return string
.length
- string
.replace(/\r*/g, '').length
;
383 function insert(block
) {
384 if (document
.selection
) {
385 var newSelection
= document
.selection
.createRange();
386 newSelection
.text
= block
;
388 textarea
.value
= textarea
.value
.substring(0, caretPosition
) + block
+ textarea
.value
.substring(caretPosition
+ selection
.length
, textarea
.value
.length
);
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) {
399 range
= textarea
.createTextRange();
400 range
.collapse(true);
401 range
.moveStart('character', start
);
402 range
.moveEnd('character', len
);
404 } else if (textarea
.setSelectionRange
){
405 textarea
.setSelectionRange(start
, start
+ len
);
407 textarea
.scrollTop
= scrollPosition
;
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
);
422 while(rangeCopy
.inRange(range
)) {
423 rangeCopy
.moveStart('character');
427 caretPosition
= textarea
.selectionStart
;
429 } else { // gecko & webkit
430 caretPosition
= textarea
.selectionStart
;
432 selection
= textarea
.value
.substring(caretPosition
, textarea
.selectionEnd
);
437 // open preview window
439 if (!previewWindow
|| previewWindow
.closed
) {
440 if (options
.previewInWindow
) {
441 previewWindow
= window
.open('', 'preview', options
.previewInWindow
);
442 $(window
).unload(function() {
443 previewWindow
.close();
446 iFrame
= $('<iframe class="markItUpPreviewFrame"></iframe>');
447 if (options
.previewPosition
== 'after') {
448 iFrame
.insertAfter(footer
);
450 iFrame
.insertBefore(header
);
452 previewWindow
= iFrame
[iFrame
.length
- 1].contentWindow
|| frame
[iFrame
.length
- 1];
454 } else if (altKey
=== true) {
458 previewWindow
.close();
460 previewWindow
= iFrame
= false;
462 if (!options
.previewAutoRefresh
) {
465 if (options
.previewInWindow
) {
466 previewWindow
.focus();
470 // refresh Preview window
471 function refreshPreview() {
475 function renderPreview() {
477 if (options
.previewParser
&& typeof options
.previewParser
=== 'function') {
478 var data
= options
.previewParser( $$.val() );
479 writeInPreview( localize(data
, 1) );
480 } else if (options
.previewParserPath
!== '') {
485 url
: options
.previewParserPath
,
486 data
: options
.previewParserVar
+'='+encodeURIComponent($$.val()),
487 success: function(data
) {
488 writeInPreview( localize(data
, 1) );
494 url
: options
.previewTemplatePath
,
497 success: function(data
) {
498 writeInPreview( localize(data
, 1).replace(/<!-- content -->/g, $$.val()) );
506 function writeInPreview(data
) {
507 if (previewWindow
.document
) {
509 sp
= previewWindow
.document
.documentElement
.scrollTop
513 previewWindow
.document
.open();
514 previewWindow
.document
.write(data
);
515 previewWindow
.document
.close();
516 previewWindow
.document
.documentElement
.scrollTop
= sp
;
521 function keyPressed(e
) {
522 shiftKey
= e
.shiftKey
;
524 ctrlKey
= (!(e
.altKey
&& e
.ctrlKey
)) ? (e
.ctrlKey
|| e
.metaKey
) : false;
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) {
531 setTimeout(function() {
532 li
.triggerHandler('mouseup');
537 if (e
.keyCode
=== 13 || e
.keyCode
=== 10) { // Enter key
538 if (ctrlKey
=== true) { // Enter + Ctrl
540 markup(options
.onCtrlEnter
);
541 return options
.onCtrlEnter
.keepDefault
;
542 } else if (shiftKey
=== true) { // Enter + Shift
544 markup(options
.onShiftEnter
);
545 return options
.onShiftEnter
.keepDefault
;
546 } else { // only Enter
547 markup(options
.onEnter
);
548 return options
.onEnter
.keepDefault
;
551 if (e
.keyCode
=== 9) { // Tab key
552 if (shiftKey
== true || ctrlKey
== true || altKey
== true) {
555 if (caretOffset
!== -1) {
557 caretOffset
= $$.val().length
- caretOffset
;
562 markup(options
.onTab
);
563 return options
.onTab
.keepDefault
;
573 $.fn
.markItUpRemove = function() {
574 return this.each(function() {
575 var $$ = $(this).unbind().removeClass('markItUpEditor');
576 $$.parent('div').parent('div.markItUp').parent('div').replaceWith($$);
581 $.markItUp = function(settings
) {
582 var options
= { target
:false };
583 $.extend(options
, settings
);
584 if (options
.target
) {
585 return $(options
.target
).each(function() {
587 $(this).trigger('insertion', [options
]);
590 $('textarea').trigger('insertion', [options
]);