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