summernote-webpack
Version:
Super simple WYSIWYG editor
747 lines (655 loc) • 20.1 kB
JavaScript
define([
'summernote/base/core/agent',
'summernote/base/core/key',
'summernote/base/core/func',
'summernote/base/core/list',
'summernote/base/core/dom',
'summernote/base/core/range',
'summernote/base/core/async',
'summernote/base/editing/History',
'summernote/base/editing/Style',
'summernote/base/editing/Typing',
'summernote/base/editing/Table',
'summernote/base/editing/Bullet'
], function (
agent, key, func, list, dom, range, async,
History, Style, Typing, Table, Bullet
) {
var KEY_BOGUS = 'bogus';
/**
* @class Editor
*/
var Editor = function (context) {
var self = this;
var $note = context.layoutInfo.note;
var $editor = context.layoutInfo.editor;
var $editable = context.layoutInfo.editable;
var options = context.options;
var lang = options.langInfo;
var editable = $editable[0];
var lastRange = null;
var style = new Style();
var table = new Table();
var typing = new Typing();
var bullet = new Bullet();
var history = new History($editable);
this.initialize = function () {
// bind custom events
$editable.on('keydown', function (event) {
if (event.keyCode === key.code.ENTER) {
context.triggerEvent('enter', event);
}
context.triggerEvent('keydown', event);
if (options.shortcuts && !event.isDefaultPrevented()) {
self.handleKeyMap(event);
}
}).on('keyup', function (event) {
context.triggerEvent('keyup', event);
}).on('focus', function (event) {
context.triggerEvent('focus', event);
}).on('blur', function (event) {
context.triggerEvent('blur', event);
}).on('mousedown', function (event) {
context.triggerEvent('mousedown', event);
}).on('mouseup', function (event) {
context.triggerEvent('mouseup', event);
}).on('scroll', function (event) {
context.triggerEvent('scroll', event);
}).on('paste', function (event) {
context.triggerEvent('paste', event);
});
// init content before set event
$editable.html(dom.html($note) || dom.emptyPara);
// [workaround] IE doesn't have input events for contentEditable
// - see: https://goo.gl/4bfIvA
var changeEventName = agent.isMSIE ? 'DOMCharacterDataModified DOMSubtreeModified DOMNodeInserted' : 'input';
$editable.on(changeEventName, function () {
context.triggerEvent('change', $editable.html());
});
$editor.on('focusin', function (event) {
context.triggerEvent('focusin', event);
}).on('focusout', function (event) {
context.triggerEvent('focusout', event);
});
if (!options.airMode && options.height) {
this.setHeight(options.height);
}
if (!options.airMode && options.maxHeight) {
$editable.css('max-height', options.maxHeight);
}
if (!options.airMode && options.minHeight) {
$editable.css('min-height', options.minHeight);
}
history.recordUndo();
};
this.destroy = function () {
$editable.off();
};
this.handleKeyMap = function (event) {
var keyMap = options.keyMap[agent.isMac ? 'mac' : 'pc'];
var keys = [];
if (event.metaKey) { keys.push('CMD'); }
if (event.ctrlKey && !event.altKey) { keys.push('CTRL'); }
if (event.shiftKey) { keys.push('SHIFT'); }
var keyName = key.nameFromCode[event.keyCode];
if (keyName) {
keys.push(keyName);
}
var eventName = keyMap[keys.join('+')];
if (eventName) {
event.preventDefault();
context.invoke(eventName);
} else if (key.isEdit(event.keyCode)) {
this.afterCommand();
}
};
/**
* create range
* @return {WrappedRange}
*/
this.createRange = function () {
this.focus();
return range.create(editable);
};
/**
* saveRange
*
* save current range
*
* @param {Boolean} [thenCollapse=false]
*/
this.saveRange = function (thenCollapse) {
lastRange = this.createRange();
if (thenCollapse) {
lastRange.collapse().select();
}
};
/**
* restoreRange
*
* restore lately range
*/
this.restoreRange = function () {
if (lastRange) {
lastRange.select();
this.focus();
}
};
this.saveTarget = function (node) {
$editable.data('target', node);
};
this.clearTarget = function () {
$editable.removeData('target');
};
this.restoreTarget = function () {
return $editable.data('target');
};
/**
* currentStyle
*
* current style
* @return {Object|Boolean} unfocus
*/
this.currentStyle = function () {
var rng = range.create();
if (rng) {
rng = rng.normalize();
}
return rng ? style.current(rng) : style.fromNode($editable);
};
/**
* style from node
*
* @param {jQuery} $node
* @return {Object}
*/
this.styleFromNode = function ($node) {
return style.fromNode($node);
};
/**
* undo
*/
this.undo = function () {
context.triggerEvent('before.command', $editable.html());
history.undo();
context.triggerEvent('change', $editable.html());
};
context.memo('help.undo', lang.help.undo);
/**
* redo
*/
this.redo = function () {
context.triggerEvent('before.command', $editable.html());
history.redo();
context.triggerEvent('change', $editable.html());
};
context.memo('help.redo', lang.help.redo);
/**
* before command
*/
var beforeCommand = this.beforeCommand = function () {
context.triggerEvent('before.command', $editable.html());
// keep focus on editable before command execution
self.focus();
};
/**
* after command
* @param {Boolean} isPreventTrigger
*/
var afterCommand = this.afterCommand = function (isPreventTrigger) {
history.recordUndo();
if (!isPreventTrigger) {
context.triggerEvent('change', $editable.html());
}
};
/* jshint ignore:start */
// native commands(with execCommand), generate function for execCommand
var commands = ['bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript',
'justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull',
'formatBlock', 'removeFormat',
'backColor', 'foreColor', 'fontName'];
for (var idx = 0, len = commands.length; idx < len; idx ++) {
this[commands[idx]] = (function (sCmd) {
return function (value) {
beforeCommand();
document.execCommand(sCmd, false, value);
afterCommand(true);
};
})(commands[idx]);
context.memo('help.' + commands[idx], lang.help[commands[idx]]);
}
/* jshint ignore:end */
/**
* handle tab key
*/
this.tab = function () {
var rng = this.createRange();
if (rng.isCollapsed() && rng.isOnCell()) {
table.tab(rng);
} else {
beforeCommand();
typing.insertTab(rng, options.tabSize);
afterCommand();
}
};
context.memo('help.tab', lang.help.tab);
/**
* handle shift+tab key
*/
this.untab = function () {
var rng = this.createRange();
if (rng.isCollapsed() && rng.isOnCell()) {
table.tab(rng, true);
}
};
context.memo('help.untab', lang.help.untab);
/**
* run given function between beforeCommand and afterCommand
*/
this.wrapCommand = function (fn) {
return function () {
beforeCommand();
fn.apply(self, arguments);
afterCommand();
};
};
/**
* insert paragraph
*/
this.insertParagraph = this.wrapCommand(function () {
typing.insertParagraph(editable);
});
context.memo('help.insertParagraph', lang.help.insertParagraph);
this.insertOrderedList = this.wrapCommand(function () {
bullet.insertOrderedList(editable);
});
context.memo('help.insertOrderedList', lang.help.insertOrderedList);
this.insertUnorderedList = this.wrapCommand(function () {
bullet.insertUnorderedList(editable);
});
context.memo('help.insertUnorderedList', lang.help.insertUnorderedList);
this.indent = this.wrapCommand(function () {
bullet.indent(editable);
});
context.memo('help.indent', lang.help.indent);
this.outdent = this.wrapCommand(function () {
bullet.outdent(editable);
});
context.memo('help.outdent', lang.help.outdent);
/**
* insert image
*
* @param {String} src
* @param {String|Function} param
* @return {Promise}
*/
this.insertImage = function (src, param) {
return async.createImage(src, param).then(function ($image) {
beforeCommand();
if (typeof param === 'function') {
param($image);
} else {
if (typeof param === 'string') {
$image.attr('data-filename', param);
}
$image.css('width', Math.min($editable.width(), $image.width()));
}
$image.show();
range.create(editable).insertNode($image[0]);
range.createFromNodeAfter($image[0]).select();
afterCommand();
}).fail(function (e) {
context.triggerEvent('image.upload.error', e);
});
};
/**
* insertImages
* @param {File[]} files
*/
this.insertImages = function (files) {
$.each(files, function (idx, file) {
var filename = file.name;
if (options.maximumImageFileSize && options.maximumImageFileSize < file.size) {
context.triggerEvent('image.upload.error', lang.image.maximumFileSizeError);
} else {
async.readFileAsDataURL(file).then(function (dataURL) {
return self.insertImage(dataURL, filename);
}).fail(function () {
context.triggerEvent('image.upload.error');
});
}
});
};
/**
* insertImagesOrCallback
* @param {File[]} files
*/
this.insertImagesOrCallback = function (files) {
var callbacks = options.callbacks;
// If onImageUpload options setted
if (callbacks.onImageUpload) {
context.triggerEvent('image.upload', files);
// else insert Image as dataURL
} else {
this.insertImages(files);
}
};
/**
* insertNode
* insert node
* @param {Node} node
*/
this.insertNode = this.wrapCommand(function (node) {
var rng = this.createRange();
rng.insertNode(node);
range.createFromNodeAfter(node).select();
});
/**
* insert text
* @param {String} text
*/
this.insertText = this.wrapCommand(function (text) {
var rng = this.createRange();
var textNode = rng.insertNode(dom.createText(text));
range.create(textNode, dom.nodeLength(textNode)).select();
});
/**
* return selected plain text
* @return {String} text
*/
this.getSelectedText = function () {
var rng = this.createRange();
// if range on anchor, expand range with anchor
if (rng.isOnAnchor()) {
rng = range.createFromNode(dom.ancestor(rng.sc, dom.isAnchor));
}
return rng.toString();
};
/**
* paste HTML
* @param {String} markup
*/
this.pasteHTML = this.wrapCommand(function (markup) {
var contents = this.createRange().pasteHTML(markup);
range.createFromNodeAfter(list.last(contents)).select();
});
/**
* formatBlock
*
* @param {String} tagName
*/
this.formatBlock = this.wrapCommand(function (tagName) {
// [workaround] for MSIE, IE need `<`
tagName = agent.isMSIE ? '<' + tagName + '>' : tagName;
document.execCommand('FormatBlock', false, tagName);
});
this.formatPara = function () {
this.formatBlock('P');
};
context.memo('help.formatPara', lang.help.formatPara);
/* jshint ignore:start */
for (var idx = 1; idx <= 6; idx ++) {
this['formatH' + idx] = function (idx) {
return function () {
this.formatBlock('H' + idx);
};
}(idx);
context.memo('help.formatH'+idx, lang.help['formatH' + idx]);
};
/* jshint ignore:end */
/**
* fontSize
*
* @param {String} value - px
*/
this.fontSize = function (value) {
var rng = this.createRange();
if (rng && rng.isCollapsed()) {
var spans = style.styleNodes(rng);
var firstSpan = list.head(spans);
$(spans).css({
'font-size': value + 'px'
});
// [workaround] added styled bogus span for style
// - also bogus character needed for cursor position
if (firstSpan && !dom.nodeLength(firstSpan)) {
firstSpan.innerHTML = dom.ZERO_WIDTH_NBSP_CHAR;
range.createFromNodeAfter(firstSpan.firstChild).select();
$editable.data(KEY_BOGUS, firstSpan);
}
} else {
beforeCommand();
$(style.styleNodes(rng)).css({
'font-size': value + 'px'
});
afterCommand();
}
};
/**
* insert horizontal rule
*/
this.insertHorizontalRule = this.wrapCommand(function () {
var hrNode = this.createRange().insertNode(dom.create('HR'));
if (hrNode.nextSibling) {
range.create(hrNode.nextSibling, 0).normalize().select();
}
});
context.memo('help.insertHorizontalRule', lang.help.insertHorizontalRule);
/**
* remove bogus node and character
*/
this.removeBogus = function () {
var bogusNode = $editable.data(KEY_BOGUS);
if (!bogusNode) {
return;
}
var textNode = list.find(list.from(bogusNode.childNodes), dom.isText);
var bogusCharIdx = textNode.nodeValue.indexOf(dom.ZERO_WIDTH_NBSP_CHAR);
if (bogusCharIdx !== -1) {
textNode.deleteData(bogusCharIdx, 1);
}
if (dom.isEmpty(bogusNode)) {
dom.remove(bogusNode);
}
$editable.removeData(KEY_BOGUS);
};
/**
* lineHeight
* @param {String} value
*/
this.lineHeight = this.wrapCommand(function (value) {
style.stylePara(this.createRange(), {
lineHeight: value
});
});
/**
* unlink
*
* @type command
*/
this.unlink = function () {
var rng = this.createRange();
if (rng.isOnAnchor()) {
var anchor = dom.ancestor(rng.sc, dom.isAnchor);
rng = range.createFromNode(anchor);
rng.select();
beforeCommand();
document.execCommand('unlink');
afterCommand();
}
};
/**
* create link (command)
*
* @param {Object} linkInfo
*/
this.createLink = this.wrapCommand(function (linkInfo) {
var linkUrl = linkInfo.url;
var linkText = linkInfo.text;
var isNewWindow = linkInfo.isNewWindow;
var rng = linkInfo.range || this.createRange();
var isTextChanged = rng.toString() !== linkText;
if (options.onCreateLink) {
linkUrl = options.onCreateLink(linkUrl);
}
var anchors = [];
if (isTextChanged) {
rng = rng.deleteContents();
var anchor = rng.insertNode($('<A>' + linkText + '</A>')[0]);
anchors.push(anchor);
} else {
anchors = style.styleNodes(rng, {
nodeName: 'A',
expandClosestSibling: true,
onlyPartialContains: true
});
}
$.each(anchors, function (idx, anchor) {
$(anchor).attr('href', linkUrl);
if (isNewWindow) {
$(anchor).attr('target', '_blank');
} else {
$(anchor).removeAttr('target');
}
});
var startRange = range.createFromNodeBefore(list.head(anchors));
var startPoint = startRange.getStartPoint();
var endRange = range.createFromNodeAfter(list.last(anchors));
var endPoint = endRange.getEndPoint();
range.create(
startPoint.node,
startPoint.offset,
endPoint.node,
endPoint.offset
).select();
});
/**
* returns link info
*
* @return {Object}
* @return {WrappedRange} return.range
* @return {String} return.text
* @return {Boolean} [return.isNewWindow=true]
* @return {String} [return.url=""]
*/
this.getLinkInfo = function () {
var rng = this.createRange().expand(dom.isAnchor);
// Get the first anchor on range(for edit).
var $anchor = $(list.head(rng.nodes(dom.isAnchor)));
return {
range: rng,
text: rng.toString(),
isNewWindow: $anchor.length ? $anchor.attr('target') === '_blank' : false,
url: $anchor.length ? $anchor.attr('href') : ''
};
};
/**
* setting color
*
* @param {Object} sObjColor color code
* @param {String} sObjColor.foreColor foreground color
* @param {String} sObjColor.backColor background color
*/
this.color = this.wrapCommand(function (colorInfo) {
var foreColor = colorInfo.foreColor;
var backColor = colorInfo.backColor;
if (foreColor) { document.execCommand('foreColor', false, foreColor); }
if (backColor) { document.execCommand('backColor', false, backColor); }
});
/**
* insert Table
*
* @param {String} dimension of table (ex : "5x5")
*/
this.insertTable = this.wrapCommand(function (dim) {
var dimension = dim.split('x');
var rng = this.createRange().deleteContents();
rng.insertNode(table.createTable(dimension[0], dimension[1], options));
});
/**
* float me
*
* @param {String} value
*/
this.floatMe = this.wrapCommand(function (value) {
var $target = $(this.restoreTarget());
$target.css('float', value);
});
/**
* resize overlay element
* @param {String} value
*/
this.resize = this.wrapCommand(function (value) {
var $target = $(this.restoreTarget());
$target.css({
width: value * 100 + '%',
height: ''
});
});
/**
* @param {Position} pos
* @param {jQuery} $target - target element
* @param {Boolean} [bKeepRatio] - keep ratio
*/
this.resizeTo = function (pos, $target, bKeepRatio) {
var imageSize;
if (bKeepRatio) {
var newRatio = pos.y / pos.x;
var ratio = $target.data('ratio');
imageSize = {
width: ratio > newRatio ? pos.x : pos.y / ratio,
height: ratio > newRatio ? pos.x * ratio : pos.y
};
} else {
imageSize = {
width: pos.x,
height: pos.y
};
}
$target.css(imageSize);
};
/**
* remove media object
*/
this.removeMedia = this.wrapCommand(function () {
var $target = $(this.restoreTarget()).detach();
context.triggerEvent('media.delete', $target, $editable);
});
/**
* returns whether editable area has focus or not.
*/
this.hasFocus = function () {
return $editable.is(':focus');
};
/**
* set focus
*/
this.focus = function () {
// [workaround] Screen will move when page is scolled in IE.
// - do focus when not focused
if (!this.hasFocus()) {
$editable.focus();
}
};
/**
* returns whether contents is empty or not.
* @return {Boolean}
*/
this.isEmpty = function () {
return dom.isEmpty($editable[0]) || dom.emptyPara === $editable.html();
};
/**
* Removes all contents and restores the editable instance to an _emptyPara_.
*/
this.empty = function () {
context.invoke('code', dom.emptyPara);
};
/**
* set height for editable
*/
this.setHeight = function (height) {
$editable.outerHeight(height);
};
};
return Editor;
});