PluginProbe
Page Builder: Pagelayer – Drag and Drop website builder / 2.1.3
Page Builder: Pagelayer – Drag and Drop website builder v2.1.3
2.2.1 2.2.0 2.1.9 2.1.8 2.1.7 2.1.6 2.1.5 2.1.4 2.1.3 trunk 0.9.0 0.9.1 0.9.2 0.9.3 0.9.4 0.9.5 0.9.6 0.9.7 0.9.8 0.9.9 1.0.0 1.0.2 1.0.3 1.0.4 1.0.5 All 129 releases
pagelayer / js / pen.js

pen.js in Page Builder: Pagelayer – Drag and Drop website builder 2.1.3, at js/pen.js

870 lines 26.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /*! Licensed under MIT, https://github.com/sofish/pen */
2 (function(root, doc) {
3
4 var Pen, debugMode, selection, utils = {};
5 var toString = Object.prototype.toString;
6 var slice = Array.prototype.slice;
7
8 // allow command list
9 var commandsReg = {
10 block: /^(?:p|h[1-6]|blockquote|pre)$/,
11 inline: /^(?:bold|italic|underline|insertorderedlist|insertunorderedlist|indent|outdent)$/,
12 source: /^(?:createlink|unlink)$/,
13 insert: /^(?:inserthorizontalrule|insertimage|insert)$/,
14 wrap: /^(?:code)$/
15 };
16
17 var lineBreakReg = /^(?:blockquote|pre|div)$/i;
18
19 var effectNodeReg = /(?:[pubia]|h[1-6]|blockquote|[uo]l|li)/i;
20
21 var strReg = {
22 whiteSpace: /(^\s+)|(\s+$)/g,
23 mailTo: /^(?!mailto:|.+\/|.+#|.+\?)(.*@.*\..+)$/,
24 http: /^(?!\w+?:\/\/|mailto:|\/|\.\/|\?|#)(.*)$/
25 };
26
27 var autoLinkReg = {
28 url: /((https?|ftp):\/\/|www\.)[^\s<]{3,}/gi,
29 prefix: /^(?:https?|ftp):\/\//i,
30 notLink: /^(?:img|a|input|audio|video|source|code|pre|script|head|title|style)$/i,
31 maxLength: 100
32 };
33
34 // type detect
35 utils.is = function(obj, type) {
36 return toString.call(obj).slice(8, -1) === type;
37 };
38
39 utils.forEach = function(obj, iterator, arrayLike) {
40 if (!obj) return;
41 if (arrayLike == null) arrayLike = utils.is(obj, 'Array');
42 if (arrayLike) {
43 for (var i = 0, l = obj.length; i < l; i++) iterator(obj[i], i, obj);
44 } else {
45 for (var key in obj) {
46 if (obj.hasOwnProperty(key)) iterator(obj[key], key, obj);
47 }
48 }
49 };
50
51 // copy props from a obj
52 utils.copy = function(defaults, source) {
53 utils.forEach(source, function (value, key) {
54 defaults[key] = utils.is(value, 'Object') ? utils.copy({}, value) :
55 utils.is(value, 'Array') ? utils.copy([], value) : value;
56 });
57 return defaults;
58 };
59
60 // log
61 utils.log = function(message, force) {
62 if (debugMode || force)
63 console.log('%cPEN DEBUGGER: %c' + message, 'font-family:arial,sans-serif;color:#1abf89;line-height:2em;', 'font-family:cursor,monospace;color:#333;');
64 };
65
66 utils.delayExec = function (fn) {
67 var timer = null;
68 return function (delay) {
69 clearTimeout(timer);
70 timer = setTimeout(function() {
71 fn();
72 }, delay || 1);
73 };
74 };
75
76 // merge: make it easy to have a fallback
77 utils.merge = function(config) {
78
79 // default settings
80 var defaults = {
81 class: 'pen',
82 debug: false,
83 toolbar: null, // custom toolbar
84 stay: config.stay || !config.debug,
85 stayMsg: 'Are you going to leave here?',
86 textarea: '<textarea name="content"></textarea>',
87 list: [
88 'blockquote', 'h2', 'h3', 'p', 'code', 'insertorderedlist', 'insertunorderedlist', 'inserthorizontalrule',
89 'indent', 'outdent', 'bold', 'italic', 'underline', 'createlink', 'insertimage'
90 ],
91 titles: {},
92 cleanAttrs: ['id', 'class', 'style', 'name'],
93 cleanTags: ['script'],
94 linksInNewWindow: false
95 };
96
97 // user-friendly config
98 if (config.nodeType === 1) {
99 defaults.editor = config;
100 } else if (config.match && config.match(/^#[\S]+$/)) {
101 defaults.editor = doc.getElementById(config.slice(1));
102 } else {
103 defaults = utils.copy(defaults, config);
104 }
105
106 return defaults;
107 };
108
109 function commandOverall(ctx, cmd, val) {
110 var message = ' to exec 「' + cmd + '」 command' + (val ? (' with value: ' + val) : '');
111
112 try {
113 doc.execCommand(cmd, false, val);
114 } catch(err) {
115 // TODO: there's an error when insert a image to document, but not a bug
116 return utils.log('fail' + message, true);
117 }
118
119 utils.log('success' + message);
120 }
121
122 function commandInsert(ctx, name, val) {
123 var node = getNode(ctx);
124 if (!node) return;
125 ctx._range.selectNode(node);
126 ctx._range.collapse(false);
127
128 // hide menu when a image was inserted
129 if(name === 'insertimage' && ctx._menu) toggleNode(ctx._menu, true);
130
131 return commandOverall(ctx, name, val);
132 }
133
134 function commandBlock(ctx, name) {
135 var list = effectNode(ctx, getNode(ctx), true);
136 if (list.indexOf(name) !== -1) name = 'p';
137 return commandOverall(ctx, 'formatblock', name);
138 }
139
140 function commandWrap(ctx, tag, value) {
141 value = '<' + tag + '>' + (value||selection.toString()) + '</' + tag + '>';
142 return commandOverall(ctx, 'insertHTML', value);
143 }
144
145 function commandLink(ctx, tag, value) {
146 if (ctx.config.linksInNewWindow) {
147 value = '< a href="' + value + '" target="_blank">' + (selection.toString()) + '</a>';
148 return commandOverall(ctx, 'insertHTML', value);
149 } else {
150 return commandOverall(ctx, tag, value);
151 }
152 }
153
154 function initToolbar(ctx) {
155 var icons = '', inputStr = '<input class="pen-input" placeholder="http://" />';
156
157 ctx._toolbar = ctx.config.toolbar;
158 if (!ctx._toolbar) {
159 var toolList = ctx.config.list;
160 utils.forEach(toolList, function (name) {
161 var klass = 'pen-icon icon-' + name;
162 var title = ctx.config.titles[name] || '';
163 icons += '<i class="' + klass + '" data-action="' + name + '" title="' + title + '"></i>';
164 }, true);
165 if (toolList.indexOf('createlink') >= 0 || toolList.indexOf('insertimage') >= 0)
166 icons += inputStr;
167 } else if (ctx._toolbar.querySelectorAll('[data-action=createlink]').length ||
168 ctx._toolbar.querySelectorAll('[data-action=insertimage]').length) {
169 icons += inputStr;
170 }
171
172 if (icons) {
173 ctx._menu = doc.createElement('div');
174 ctx._menu.setAttribute('class', ctx.config.class + '-menu pen-menu');
175 ctx._menu.innerHTML = icons;
176 ctx._inputBar = ctx._menu.querySelector('input');
177 toggleNode(ctx._menu, true);
178 doc.body.appendChild(ctx._menu);
179 }
180 if (ctx._toolbar && ctx._inputBar) toggleNode(ctx._inputBar);
181 }
182
183 function initEvents(ctx) {
184 var toolbar = ctx._toolbar || ctx._menu, editor = ctx.config.editor;
185
186 var toggleMenu = utils.delayExec(function() {
187 ctx.highlight().menu();
188 });
189 var outsideClick = function() {};
190
191 function updateStatus(delay) {
192 ctx._range = ctx.getRange();
193 toggleMenu(delay);
194 }
195
196 if (ctx._menu) {
197 var setpos = function() {
198 if (ctx._menu.style.display === 'block') ctx.menu();
199 };
200
201 // change menu offset when window resize / scroll
202 addListener(ctx, root, 'resize', setpos);
203 addListener(ctx, root, 'scroll', setpos);
204
205 // toggle toolbar on mouse select
206 var selecting = false;
207 addListener(ctx, editor, 'mousedown', function() {
208 selecting = true;
209 });
210 addListener(ctx, editor, 'mouseleave', function() {
211 if (selecting) updateStatus(800);
212 selecting = false;
213 });
214 addListener(ctx, editor, 'mouseup', function() {
215 if (selecting) updateStatus(100);
216 selecting = false;
217 });
218 // Hide menu when focusing outside of editor
219 outsideClick = function(e) {
220 if (ctx._menu && !containsNode(editor, e.target) && !containsNode(ctx._menu, e.target)) {
221 removeListener(ctx, doc, 'click', outsideClick);
222 toggleMenu(100);
223 }
224 };
225 } else {
226 addListener(ctx, editor, 'click', function() {
227 updateStatus(0);
228 });
229 }
230
231 addListener(ctx, editor, 'keyup', function(e) {
232 if (e.which === 8 && ctx.isEmpty()) return lineBreak(ctx, true);
233 // toggle toolbar on key select
234 if (e.which !== 13 || e.shiftKey) return updateStatus(400);
235 var node = getNode(ctx, true);
236 if (!node || !node.nextSibling || !lineBreakReg.test(node.nodeName)) return;
237 if (node.nodeName !== node.nextSibling.nodeName) return;
238 // hack for webkit, make 'enter' behavior like as firefox.
239 if (node.lastChild.nodeName !== 'BR') node.appendChild(doc.createElement('br'));
240 utils.forEach(node.nextSibling.childNodes, function(child) {
241 if (child) node.appendChild(child);
242 }, true);
243 node.parentNode.removeChild(node.nextSibling);
244 focusNode(ctx, node.lastChild, ctx.getRange());
245 });
246
247 // check line break
248 addListener(ctx, editor, 'keydown', function(e) {
249 editor.classList.remove('pen-placeholder');
250 if (e.which !== 13 || e.shiftKey) return;
251 var node = getNode(ctx, true);
252 if (!node || !lineBreakReg.test(node.nodeName)) return;
253 var lastChild = node.lastChild;
254 if (!lastChild || !lastChild.previousSibling) return;
255 if (lastChild.previousSibling.textContent || lastChild.textContent) return;
256 // quit block mode for 2 'enter'
257 e.preventDefault();
258 var p = doc.createElement('p');
259 p.innerHTML = '<br>';
260 node.removeChild(lastChild);
261 if (!node.nextSibling) node.parentNode.appendChild(p);
262 else node.parentNode.insertBefore(p, node.nextSibling);
263 focusNode(ctx, p, ctx.getRange());
264 });
265
266 var menuApply = function(action, value) {
267 ctx.execCommand(action, value);
268 ctx._range = ctx.getRange();
269 ctx.highlight().menu();
270 };
271
272 // toggle toolbar on key select
273 addListener(ctx, toolbar, 'click', function(e) {
274 var node = e.target, action;
275
276 while (node !== toolbar && !(action = node.getAttribute('data-action'))) {
277 node = node.parentNode;
278 }
279
280 if (!action) return;
281 if (!/(?:createlink)|(?:insertimage)/.test(action)) return menuApply(action);
282 if (!ctx._inputBar) return;
283
284 // create link
285 var input = ctx._inputBar;
286 if (toolbar === ctx._menu) toggleNode(input);
287 else {
288 ctx._inputActive = true;
289 ctx.menu();
290 }
291 if (ctx._menu.style.display === 'none') return;
292
293 setTimeout(function() { input.focus(); }, 400);
294 var createlink = function() {
295 var inputValue = input.value;
296
297 if (!inputValue) action = 'unlink';
298 else {
299 inputValue = input.value
300 .replace(strReg.whiteSpace, '')
301 .replace(strReg.mailTo, 'mailto:$1')
302 .replace(strReg.http, 'http://$1');
303 }
304 menuApply(action, inputValue);
305 if (toolbar === ctx._menu) toggleNode(input, false);
306 else toggleNode(ctx._menu, true);
307 };
308
309 input.onkeypress = function(e) {
310 if (e.which === 13) return createlink();
311 };
312
313 });
314
315 // listen for placeholder
316 addListener(ctx, editor, 'focus', function() {
317 if (ctx.isEmpty()) lineBreak(ctx, true);
318 addListener(ctx, doc, 'click', outsideClick);
319 });
320
321 addListener(ctx, editor, 'blur', function() {
322 checkPlaceholder(ctx);
323 ctx.checkContentChange();
324 });
325
326 // listen for paste and clear style
327 addListener(ctx, editor, 'paste', function() {
328 setTimeout(function() {
329 ctx.cleanContent();
330 });
331 });
332 }
333
334 function addListener(ctx, target, type, listener) {
335 if (ctx._events.hasOwnProperty(type)) {
336 ctx._events[type].push(listener);
337 } else {
338 ctx._eventTargets = ctx._eventTargets || [];
339 ctx._eventsCache = ctx._eventsCache || [];
340 var index = ctx._eventTargets.indexOf(target);
341 if (index < 0) index = ctx._eventTargets.push(target) - 1;
342 ctx._eventsCache[index] = ctx._eventsCache[index] || {};
343 ctx._eventsCache[index][type] = ctx._eventsCache[index][type] || [];
344 ctx._eventsCache[index][type].push(listener);
345
346 target.addEventListener(type, listener, false);
347 }
348 return ctx;
349 }
350
351 // trigger local events
352 function triggerListener(ctx, type) {
353 if (!ctx._events.hasOwnProperty(type)) return;
354 var args = slice.call(arguments, 2);
355 utils.forEach(ctx._events[type], function (listener) {
356 listener.apply(ctx, args);
357 });
358 }
359
360 function removeListener(ctx, target, type, listener) {
361 var events = ctx._events[type];
362 if (!events) {
363 var _index = ctx._eventTargets.indexOf(target);
364 if (_index >= 0) events = ctx._eventsCache[_index][type];
365 }
366 if (!events) return ctx;
367 var index = events.indexOf(listener);
368 if (index >= 0) events.splice(index, 1);
369 target.removeEventListener(type, listener, false);
370 return ctx;
371 }
372
373 function removeAllListeners(ctx) {
374 utils.forEach(this._events, function (events) {
375 events.length = 0;
376 }, false);
377 if (!ctx._eventsCache) return ctx;
378 utils.forEach(ctx._eventsCache, function (events, index) {
379 var target = ctx._eventTargets[index];
380 utils.forEach(events, function (listeners, type) {
381 utils.forEach(listeners, function (listener) {
382 target.removeEventListener(type, listener, false);
383 }, true);
384 }, false);
385 }, true);
386 ctx._eventTargets = [];
387 ctx._eventsCache = [];
388 return ctx;
389 }
390
391 function checkPlaceholder(ctx) {
392 ctx.config.editor.classList[ctx.isEmpty() ? 'add' : 'remove']('pen-placeholder');
393 }
394
395 function trim(str) {
396 return (str || '').replace(/^\s+|\s+$/g, '');
397 }
398
399 // node.contains is not implemented in IE10/IE11
400 function containsNode(parent, child) {
401 if (parent === child) return true;
402 child = child.parentNode;
403 while (child) {
404 if (child === parent) return true;
405 child = child.parentNode;
406 }
407 return false;
408 }
409
410 function getNode(ctx, byRoot) {
411 var node, root = ctx.config.editor;
412 ctx._range = ctx._range || ctx.getRange();
413 node = ctx._range.commonAncestorContainer;
414 if (!node || node === root) return null;
415 while (node && (node.nodeType !== 1) && (node.parentNode !== root)) node = node.parentNode;
416 while (node && byRoot && (node.parentNode !== root)) node = node.parentNode;
417 return containsNode(root, node) ? node : null;
418 }
419
420 // node effects
421 function effectNode(ctx, el, returnAsNodeName) {
422 var nodes = [];
423 el = el || ctx.config.editor;
424 while (el && el !== ctx.config.editor) {
425 if (el.nodeName.match(effectNodeReg)) {
426 nodes.push(returnAsNodeName ? el.nodeName.toLowerCase() : el);
427 }
428 el = el.parentNode;
429 }
430 return nodes;
431 }
432
433 // breakout from node
434 function lineBreak(ctx, empty) {
435 var range = ctx._range = ctx.getRange(), node = doc.createElement('p');
436 if (empty) ctx.config.editor.innerHTML = '';
437 node.innerHTML = '<br>';
438 range.insertNode(node);
439 focusNode(ctx, node.childNodes[0], range);
440 }
441
442 function focusNode(ctx, node, range) {
443 range.setStartAfter(node);
444 range.setEndBefore(node);
445 range.collapse(false);
446 ctx.setRange(range);
447 }
448
449 function autoLink(node) {
450 if (node.nodeType === 1) {
451 if (autoLinkReg.notLink.test(node.tagName)) return;
452 utils.forEach(node.childNodes, function (child) {
453 autoLink(child);
454 }, true);
455 } else if (node.nodeType === 3) {
456 var result = urlToLink(node.nodeValue || '');
457 if (!result.links) return;
458 var frag = doc.createDocumentFragment(),
459 div = doc.createElement('div');
460 div.innerHTML = result.text;
461 while (div.childNodes.length) frag.appendChild(div.childNodes[0]);
462 node.parentNode.replaceChild(frag, node);
463 }
464 }
465
466 function urlToLink(str) {
467 var count = 0;
468 str = str.replace(autoLinkReg.url, function(url) {
469 var realUrl = url, displayUrl = url;
470 count++;
471 if (url.length > autoLinkReg.maxLength) displayUrl = url.slice(0, autoLinkReg.maxLength) + '...';
472 // Add http prefix if necessary
473 if (!autoLinkReg.prefix.test(realUrl)) realUrl = 'http://' + realUrl;
474 return '<a href="' + realUrl + '">' + displayUrl + '</a>';
475 });
476 return {links: count, text: str};
477 }
478
479 function toggleNode(node, hide) {
480 node.style.display = hide ? 'none' : 'block';
481 }
482
483 Pen = function(config) {
484
485 if (!config) throw new Error('Can\'t find config');
486
487 debugMode = config.debug;
488
489 // merge user config
490 var defaults = utils.merge(config);
491
492 var editor = defaults.editor;
493
494 if (!editor || editor.nodeType !== 1) throw new Error('Can\'t find editor');
495
496 // set default class
497 editor.classList.add(defaults.class);
498
499 // set contenteditable
500 editor.setAttribute('contenteditable', 'true');
501
502 // assign config
503 this.config = defaults;
504
505 // set placeholder
506 if (defaults.placeholder) editor.setAttribute('data-placeholder', defaults.placeholder);
507 checkPlaceholder(this);
508
509 // save the selection obj
510 this.selection = selection;
511
512 // define local events
513 this._events = {change: []};
514
515 // enable toolbar
516 initToolbar(this);
517
518 // init events
519 initEvents(this);
520
521 // to check content change
522 this._prevContent = this.getContent();
523
524 // enable markdown covert
525 if (this.markdown) this.markdown.init(this);
526
527 // stay on the page
528 if (this.config.stay) this.stay(this.config);
529
530 if(this.config.input) {
531 this.addOnSubmitListener(this.config.input);
532 }
533 };
534
535 Pen.prototype.on = function(type, listener) {
536 addListener(this, this.config.editor, type, listener);
537 return this;
538 };
539
540 Pen.prototype.addOnSubmitListener = function(inputElement) {
541 var form = inputElement.form;
542 var me = this;
543 form.addEventListener("submit", function() {
544 inputElement.value = me.config.saveAsMarkdown ? me.toMd(me.config.editor.innerHTML) : me.config.editor.innerHTML;
545 });
546 };
547
548 Pen.prototype.isEmpty = function(node) {
549 node = node || this.config.editor;
550 return !(node.querySelector('img')) && !(node.querySelector('blockquote')) &&
551 !(node.querySelector('li')) && !trim(node.textContent);
552 };
553
554 Pen.prototype.getContent = function() {
555 return this.isEmpty() ? '' : trim(this.config.editor.innerHTML);
556 };
557
558 Pen.prototype.setContent = function(html) {
559 this.config.editor.innerHTML = html;
560 this.cleanContent();
561 return this;
562 };
563
564 Pen.prototype.checkContentChange = function () {
565 var prevContent = this._prevContent, currentContent = this.getContent();
566 if (prevContent === currentContent) return;
567 this._prevContent = currentContent;
568 triggerListener(this, 'change', currentContent, prevContent);
569 };
570
571 Pen.prototype.getRange = function() {
572 var editor = this.config.editor, range = selection.rangeCount && selection.getRangeAt(0);
573 if (!range) range = doc.createRange();
574 if (!containsNode(editor, range.commonAncestorContainer)) {
575 range.selectNodeContents(editor);
576 range.collapse(false);
577 }
578 return range;
579 };
580
581 Pen.prototype.setRange = function(range) {
582 range = range || this._range;
583 if (!range) {
584 range = this.getRange();
585 range.collapse(false); // set to end
586 }
587 try {
588 selection.removeAllRanges();
589 selection.addRange(range);
590 } catch (e) {/* IE throws error sometimes*/}
591 return this;
592 };
593
594 Pen.prototype.focus = function(focusStart) {
595 if (!focusStart) this.setRange();
596 this.config.editor.focus();
597 return this;
598 };
599
600 Pen.prototype.execCommand = function(name, value) {
601 name = name.toLowerCase();
602 this.setRange();
603
604 if (commandsReg.block.test(name)) {
605 commandBlock(this, name);
606 } else if (commandsReg.inline.test(name)) {
607 commandOverall(this, name, value);
608 } else if (commandsReg.source.test(name)) {
609 commandLink(this, name, value);
610 } else if (commandsReg.insert.test(name)) {
611 commandInsert(this, name, value);
612 } else if (commandsReg.wrap.test(name)) {
613 commandWrap(this, name, value);
614 } else {
615 utils.log('can not find command function for name: ' + name + (value ? (', value: ' + value) : ''), true);
616 }
617 if (name === 'indent') this.checkContentChange();
618 else this.cleanContent({cleanAttrs: ['style']});
619 };
620
621 // remove attrs and tags
622 // pen.cleanContent({cleanAttrs: ['style'], cleanTags: ['id']})
623 Pen.prototype.cleanContent = function(options) {
624 var editor = this.config.editor;
625
626 if (!options) options = this.config;
627 utils.forEach(options.cleanAttrs, function (attr) {
628 utils.forEach(editor.querySelectorAll('[' + attr + ']'), function(item) {
629 item.removeAttribute(attr);
630 }, true);
631 }, true);
632 utils.forEach(options.cleanTags, function (tag) {
633 utils.forEach(editor.querySelectorAll(tag), function(item) {
634 item.parentNode.removeChild(item);
635 }, true);
636 }, true);
637
638 checkPlaceholder(this);
639 this.checkContentChange();
640 return this;
641 };
642
643 // auto link content, return content
644 Pen.prototype.autoLink = function() {
645 autoLink(this.config.editor);
646 return this.getContent();
647 };
648
649 // highlight menu
650 Pen.prototype.highlight = function() {
651 var toolbar = this._toolbar || this._menu
652 , node = getNode(this);
653 // remove all highlights
654 utils.forEach(toolbar.querySelectorAll('.active'), function(el) {
655 el.classList.remove('active');
656 }, true);
657
658 if (!node) return this;
659
660 var effects = effectNode(this, node)
661 , inputBar = this._inputBar
662 , highlight;
663
664 if (inputBar && toolbar === this._menu) {
665 // display link input if createlink enabled
666 inputBar.style.display = 'none';
667 // reset link input value
668 inputBar.value = '';
669 }
670
671 highlight = function(str) {
672 if (!str) return;
673 var el = toolbar.querySelector('[data-action=' + str + ']');
674 return el && el.classList.add('active');
675 };
676 utils.forEach(effects, function(item) {
677 var tag = item.nodeName.toLowerCase();
678 switch(tag) {
679 case 'a':
680 if (inputBar) inputBar.value = item.getAttribute('href');
681 tag = 'createlink';
682 break;
683 case 'img':
684 if (inputBar) inputBar.value = item.getAttribute('src');
685 tag = 'insertimage';
686 break;
687 case 'i':
688 tag = 'italic';
689 break;
690 case 'u':
691 tag = 'underline';
692 break;
693 case 'b':
694 tag = 'bold';
695 break;
696 case 'pre':
697 case 'code':
698 tag = 'code';
699 break;
700 case 'ul':
701 tag = 'insertunorderedlist';
702 break;
703 case 'ol':
704 tag = 'insertorderedlist';
705 break;
706 case 'li':
707 tag = 'indent';
708 break;
709 }
710 highlight(tag);
711 }, true);
712
713 return this;
714 };
715
716 // show menu
717 Pen.prototype.menu = function() {
718
719 var allMenus = document.getElementsByClassName('pen-menu');
720
721 // Hide all menus
722 for(var x in allMenus){
723 try{
724 if('style' in allMenus[x]){
725 allMenus[x].style.display = 'none';
726 }
727 }catch(e){}
728 }
729
730 if (!this._menu) return this;
731 if (selection.isCollapsed) {
732 this._menu.style.display = 'none'; //hide menu
733 this._inputActive = false;
734 return this;
735 }
736 if (this._toolbar) {
737 if (!this._inputBar || !this._inputActive) return this;
738 }
739 var offset = this._range.getBoundingClientRect()
740 , menuPadding = 10
741 , top = offset.top - menuPadding
742 , left = offset.left + (offset.width / 2)
743 , menu = this._menu
744 , menuOffset = {x: 0, y: 0}
745 , stylesheet = this._stylesheet;
746
747 // fixes some browser double click visual discontinuity
748 // if the offset has no width or height it should not be used
749 if (offset.width === 0 && offset.height === 0) return this;
750
751 // store the stylesheet used for positioning the menu horizontally
752 if (this._stylesheet === undefined) {
753 var style = document.createElement("style");
754 document.head.appendChild(style);
755 this._stylesheet = stylesheet = style.sheet;
756 }
757
758 // display block to caculate its width & height
759 menu.style.display = 'block';
760
761 menuOffset.x = left - (menu.clientWidth / 2);
762 menuOffset.y = top - menu.clientHeight;
763
764 // check to see if menu has over-extended its bounding box. if it has,
765 // 1) apply a new class if overflowed on top;
766 // 2) apply a new rule if overflowed on the left
767 if (stylesheet.cssRules.length > 0) {
768 stylesheet.deleteRule(0);
769 }
770 if (menuOffset.x < 0) {
771 menuOffset.x = 0;
772 stylesheet.insertRule('.pen-menu:after {left: ' + left + 'px;}', 0);
773 } else {
774 stylesheet.insertRule('.pen-menu:after {left: 50%; }', 0);
775 }
776 if (menuOffset.y < 0) {
777 menu.classList.add('pen-menu-below');
778 menuOffset.y = offset.top + offset.height + menuPadding;
779 } else {
780 menu.classList.remove('pen-menu-below');
781 }
782
783 menu.style.top = menuOffset.y + 'px';
784 menu.style.left = menuOffset.x + 'px';
785 return this;
786 };
787
788 Pen.prototype.stay = function(config) {
789 var ctx = this;
790 if (!window.onbeforeunload) {
791 window.onbeforeunload = function() {
792 if (!ctx._isDestroyed) return config.stayMsg;
793 };
794 }
795 };
796
797 Pen.prototype.destroy = function(isAJoke) {
798 var destroy = isAJoke ? false : true
799 , attr = isAJoke ? 'setAttribute' : 'removeAttribute';
800
801 if (!isAJoke) {
802 removeAllListeners(this);
803 try {
804 selection.removeAllRanges();
805 if (this._menu) this._menu.parentNode.removeChild(this._menu);
806 } catch (e) {/* IE throws error sometimes*/}
807 } else {
808 initToolbar(this);
809 initEvents(this);
810 }
811 this._isDestroyed = destroy;
812 this.config.editor[attr]('contenteditable', '');
813
814 return this;
815 };
816
817 Pen.prototype.rebuild = function() {
818 return this.destroy('it\'s a joke');
819 };
820
821 // a fallback for old browers
822 root.Pen = function(config) {
823 if (!config) return utils.log('can\'t find config', true);
824
825 var defaults = utils.merge(config)
826 , klass = defaults.editor.getAttribute('class');
827
828 klass = klass ? klass.replace(/\bpen\b/g, '') + ' pen-textarea ' + defaults.class : 'pen pen-textarea';
829 defaults.editor.setAttribute('class', klass);
830 defaults.editor.innerHTML = defaults.textarea;
831 return defaults.editor;
832 };
833
834 // export content as markdown
835 var regs = {
836 a: [/<a\b[^>]*href=["']([^"]+|[^']+)\b[^>]*>(.*?)<\/a>/ig, '[$2]($1)'],
837 img: [/<img\b[^>]*src=["']([^\"+|[^']+)[^>]*>/ig, '![]($1)'],
838 b: [/<b\b[^>]*>(.*?)<\/b>/ig, '**$1**'],
839 i: [/<i\b[^>]*>(.*?)<\/i>/ig, '***$1***'],
840 h: [/<h([1-6])\b[^>]*>(.*?)<\/h\1>/ig, function(a, b, c) {
841 return '\n' + ('######'.slice(0, b)) + ' ' + c + '\n';
842 }],
843 li: [/<(li)\b[^>]*>(.*?)<\/\1>/ig, '* $2\n'],
844 blockquote: [/<(blockquote)\b[^>]*>(.*?)<\/\1>/ig, '\n> $2\n'],
845 pre: [/<pre\b[^>]*>(.*?)<\/pre>/ig, '\n```\n$1\n```\n'],
846 code: [/<code\b[^>]*>(.*?)<\/code>/ig, '\n`\n$1\n`\n'],
847 p: [/<p\b[^>]*>(.*?)<\/p>/ig, '\n$1\n'],
848 hr: [/<hr\b[^>]*>/ig, '\n---\n']
849 };
850
851 Pen.prototype.toMd = function() {
852 var html = this.getContent()
853 .replace(/\n+/g, '') // remove line break
854 .replace(/<([uo])l\b[^>]*>(.*?)<\/\1l>/ig, '$2'); // remove ul/ol
855
856 for(var p in regs) {
857 if (regs.hasOwnProperty(p))
858 html = html.replace.apply(html, regs[p]);
859 }
860 return html.replace(/\*{5}/g, '**');
861 };
862
863 // make it accessible
864 if (doc.getSelection) {
865 selection = doc.getSelection();
866 root.Pen = Pen;
867 }
868
869 }(window, document));
870