alpaca
Version:
Alpaca provides the easiest and fastest way to generate interactive forms for the web and mobile devices. It runs simply as HTML5 or more elaborately using Bootstrap, jQuery Mobile or jQuery UI. Alpaca uses Handlebars to process JSON schema and provide
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;
});