pen 0.1.2 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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));