PluginProbe
Page Builder: Pagelayer – Drag and Drop website builder / 0.9.7
Page Builder: Pagelayer – Drag and Drop website builder v0.9.7
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 0.9.7, at js/pen.js

857 lines 26.0 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 if (!this._menu) return this;
719 if (selection.isCollapsed) {
720 this._menu.style.display = 'none'; //hide menu
721 this._inputActive = false;
722 return this;
723 }
724 if (this._toolbar) {
725 if (!this._inputBar || !this._inputActive) return this;
726 }
727 var offset = this._range.getBoundingClientRect()
728 , menuPadding = 10
729 , top = offset.top - menuPadding
730 , left = offset.left + (offset.width / 2)
731 , menu = this._menu
732 , menuOffset = {x: 0, y: 0}
733 , stylesheet = this._stylesheet;
734
735 // fixes some browser double click visual discontinuity
736 // if the offset has no width or height it should not be used
737 if (offset.width === 0 && offset.height === 0) return this;
738
739 // store the stylesheet used for positioning the menu horizontally
740 if (this._stylesheet === undefined) {
741 var style = document.createElement("style");
742 document.head.appendChild(style);
743 this._stylesheet = stylesheet = style.sheet;
744 }
745 // display block to caculate its width & height
746 menu.style.display = 'block';
747
748 menuOffset.x = left - (menu.clientWidth / 2);
749 menuOffset.y = top - menu.clientHeight;
750
751 // check to see if menu has over-extended its bounding box. if it has,
752 // 1) apply a new class if overflowed on top;
753 // 2) apply a new rule if overflowed on the left
754 if (stylesheet.cssRules.length > 0) {
755 stylesheet.deleteRule(0);
756 }
757 if (menuOffset.x < 0) {
758 menuOffset.x = 0;
759 stylesheet.insertRule('.pen-menu:after {left: ' + left + 'px;}', 0);
760 } else {
761 stylesheet.insertRule('.pen-menu:after {left: 50%; }', 0);
762 }
763 if (menuOffset.y < 0) {
764 menu.classList.add('pen-menu-below');
765 menuOffset.y = offset.top + offset.height + menuPadding;
766 } else {
767 menu.classList.remove('pen-menu-below');
768 }
769
770 menu.style.top = menuOffset.y + 'px';
771 menu.style.left = menuOffset.x + 'px';
772 return this;
773 };
774
775 Pen.prototype.stay = function(config) {
776 var ctx = this;
777 if (!window.onbeforeunload) {
778 window.onbeforeunload = function() {
779 if (!ctx._isDestroyed) return config.stayMsg;
780 };
781 }
782 };
783
784 Pen.prototype.destroy = function(isAJoke) {
785 var destroy = isAJoke ? false : true
786 , attr = isAJoke ? 'setAttribute' : 'removeAttribute';
787
788 if (!isAJoke) {
789 removeAllListeners(this);
790 try {
791 selection.removeAllRanges();
792 if (this._menu) this._menu.parentNode.removeChild(this._menu);
793 } catch (e) {/* IE throws error sometimes*/}
794 } else {
795 initToolbar(this);
796 initEvents(this);
797 }
798 this._isDestroyed = destroy;
799 this.config.editor[attr]('contenteditable', '');
800
801 return this;
802 };
803
804 Pen.prototype.rebuild = function() {
805 return this.destroy('it\'s a joke');
806 };
807
808 // a fallback for old browers
809 root.Pen = function(config) {
810 if (!config) return utils.log('can\'t find config', true);
811
812 var defaults = utils.merge(config)
813 , klass = defaults.editor.getAttribute('class');
814
815 klass = klass ? klass.replace(/\bpen\b/g, '') + ' pen-textarea ' + defaults.class : 'pen pen-textarea';
816 defaults.editor.setAttribute('class', klass);
817 defaults.editor.innerHTML = defaults.textarea;
818 return defaults.editor;
819 };
820
821 // export content as markdown
822 var regs = {
823 a: [/<a\b[^>]*href=["']([^"]+|[^']+)\b[^>]*>(.*?)<\/a>/ig, '[$2]($1)'],
824 img: [/<img\b[^>]*src=["']([^\"+|[^']+)[^>]*>/ig, '![]($1)'],
825 b: [/<b\b[^>]*>(.*?)<\/b>/ig, '**$1**'],
826 i: [/<i\b[^>]*>(.*?)<\/i>/ig, '***$1***'],
827 h: [/<h([1-6])\b[^>]*>(.*?)<\/h\1>/ig, function(a, b, c) {
828 return '\n' + ('######'.slice(0, b)) + ' ' + c + '\n';
829 }],
830 li: [/<(li)\b[^>]*>(.*?)<\/\1>/ig, '* $2\n'],
831 blockquote: [/<(blockquote)\b[^>]*>(.*?)<\/\1>/ig, '\n> $2\n'],
832 pre: [/<pre\b[^>]*>(.*?)<\/pre>/ig, '\n```\n$1\n```\n'],
833 code: [/<code\b[^>]*>(.*?)<\/code>/ig, '\n`\n$1\n`\n'],
834 p: [/<p\b[^>]*>(.*?)<\/p>/ig, '\n$1\n'],
835 hr: [/<hr\b[^>]*>/ig, '\n---\n']
836 };
837
838 Pen.prototype.toMd = function() {
839 var html = this.getContent()
840 .replace(/\n+/g, '') // remove line break
841 .replace(/<([uo])l\b[^>]*>(.*?)<\/\1l>/ig, '$2'); // remove ul/ol
842
843 for(var p in regs) {
844 if (regs.hasOwnProperty(p))
845 html = html.replace.apply(html, regs[p]);
846 }
847 return html.replace(/\*{5}/g, '**');
848 };
849
850 // make it accessible
851 if (doc.getSelection) {
852 selection = doc.getSelection();
853 root.Pen = Pen;
854 }
855
856 }(window, document));
857