pen 0.1.2 → 0.2.2

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