<NME> Editor.js <BEF> define([ 'materialnote/base/core/agent', 'materialnote/base/core/key', 'materialnote/base/core/func', 'materialnote/base/core/list', 'materialnote/base/core/dom', 'materialnote/base/core/range', 'materialnote/base/core/async', 'materialnote/base/editing/History', 'materialnote/base/editing/Style', 'materialnote/base/editing/Typing', 'materialnote/base/editing/Table', 'materialnote/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 (!event.isDefaultPrevented()) { if (options.shortcuts) { self.handleKeyMap(event); } else { self.preventDefaultEditableShortCuts(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, func.debounce(function () { context.triggerEvent('change', $editable.html()); }, 250)); $editor.on('focusin', function (event) { context.triggerEvent('focusin', event); }).on('focusout', function (event) { context.triggerEvent('focusout', event); }); if (!options.airMode) { if (options.width) { $editor.outerWidth(options.width); } if (options.height) { $editable.outerHeight(options.height); } if (options.maxHeight) { $editable.css('max-height', options.maxHeight); } if (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(); } }; this.preventDefaultEditableShortCuts = function (event) { // B(Bold, 66) / I(Italic, 73) / U(Underline, 85) if ((event.ctrlKey || event.metaKey) && list.contains([66, 73, 85], event.keyCode)) { event.preventDefault(); } }; /** * 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, $target) { var onApplyCustomStyle = context.options.callbacks.onApplyCustomStyle; if (onApplyCustomStyle) { onApplyCustomStyle.call(this, $target, context, this.onFormatBlock); } else { this.onFormatBlock(tagName); } }); this.onFormatBlock = 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; // handle spaced urls from input if (typeof linkUrl === 'string') { linkUrl = linkUrl.trim(); } 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))); var linkInfo = { range: rng, text: rng.toString(), url: $anchor.length ? $anchor.attr('href') : '' }; // Define isNewWindow when anchor exists. if ($anchor.length) { linkInfo.isNewWindow = $anchor.attr('target') === '_blank'; } return linkInfo; }; /** * 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 (tableOptions) { var dimension = tableOptions.dim.split('x'); var rng = this.createRange().deleteContents(); rng.insertNode(table.createTable(dimension[0], dimension[1], tableOptions)); }); /** * @method addRow * * */ this.addRow = function (position) { var rng = this.createRange($editable); if (rng.isCollapsed() && rng.isOnCell()) { beforeCommand(); table.addRow(rng, position); afterCommand(); } }; /** * @method addCol * * */ this.addCol = function (position) { var rng = this.createRange($editable); if (rng.isCollapsed() && rng.isOnCell()) { beforeCommand(); table.addCol(rng, position); afterCommand(); } }; /** * @method deleteRow * * */ this.deleteRow = function () { var rng = this.createRange($editable); if (rng.isCollapsed() && rng.isOnCell()) { beforeCommand(); table.deleteRow(rng); afterCommand(); } }; /** * @method deleteCol * * */ this.deleteCol = function () { var rng = this.createRange($editable); if (rng.isCollapsed() && rng.isOnCell()) { beforeCommand(); table.deleteCol(rng); afterCommand(); } }; /** * @method deleteTable * * */ this.deleteTable = function () { var rng = this.createRange($editable); if (rng.isCollapsed() && rng.isOnCell()) { beforeCommand(); table.deleteTable(rng); afterCommand(); } }; /** * 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); }; }; return Editor; }); sel.addRange(range); } else { context.triggerEvent('change'); } } }; /** * @method deleteCol * * */ this.deleteCol = function () { let rng = this.createRange($editable); let cell = dom.ancestor(rng.commonAncestor(), dom.isCell); let $cell = $(cell); // set newTarget to prev or next column, same row let newTarget = $cell.prev('td'); if (newTarget.length === 0) { newTarget = $cell.next('td'); } if (rng.isCollapsed() && rng.isOnCell()) { beforeCommand(); table.deleteCol(rng); afterCommand(true); // move popover to new target (keep it opened) if (newTarget.length !== 0) { context.triggerEvent('change', newTarget[0]); let range = document.createRange(); let sel = window.getSelection(); range.setStart(newTarget[0], 1); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); } else { context.triggerEvent('change'); } } }; /** * @method deleteTable * * */ this.deleteTable = function () { var rng = this.createRange($editable); if (rng.isCollapsed() && rng.isOnCell()) { beforeCommand(); table.deleteTable(rng); afterCommand(); } }; /** * float me * * @param {String} value */ this.floatMe = function(value) { var $target = $(this.restoreTarget()); beforeCommand(); $target.css('float', value); afterCommand(true); context.triggerEvent('change', $target[0]); }; /** * resize overlay element * @param {String} value */ this.resize = function(dim) { let $target = $(this.restoreTarget()); let styles = context.getInlineStyles($target); let percentageDim = dim * 100 + '%'; beforeCommand(); if (styles.width === percentageDim) { $target.css({ width: 'auto' }); } else { $target.css({ width: percentageDim, height: '' }); } afterCommand(true); context.triggerEvent('change', $target[0]); }; /** * add or remove materialize's responsive class to image */ this.responsivize = function() { let $target = $(this.restoreTarget()); beforeCommand(); $target.toggleClass('responsive-img'); afterCommand(true); context.triggerEvent('change', $target[0]); }; /** * add or remove target="_blank" to/from a link */ this.toggleOpenInNewWindow = function() { let rng = this.createRange().expand(dom.isAnchor); // Get the first anchor on range(for edit). let $anchor = $(list.head(rng.nodes(dom.isAnchor))); let linkTarget = $anchor.attr('target'); beforeCommand(); linkTarget === '_blank' ? $anchor.attr('target', '_self') : $anchor.attr('target', '_blank'); afterCommand(true); context.triggerEvent('change', $anchor[0]); }; /** * @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); }; }; return Editor; }); <MSG> added responsive option to imageDialog <DFF> @@ -1,846 +1,863 @@ define([ - 'materialnote/base/core/agent', - 'materialnote/base/core/key', - 'materialnote/base/core/func', - 'materialnote/base/core/list', - 'materialnote/base/core/dom', - 'materialnote/base/core/range', - 'materialnote/base/core/async', - 'materialnote/base/editing/History', - 'materialnote/base/editing/Style', - 'materialnote/base/editing/Typing', - 'materialnote/base/editing/Table', - 'materialnote/base/editing/Bullet' + 'materialnote/base/core/agent', + 'materialnote/base/core/key', + 'materialnote/base/core/func', + 'materialnote/base/core/list', + 'materialnote/base/core/dom', + 'materialnote/base/core/range', + 'materialnote/base/core/async', + 'materialnote/base/editing/History', + 'materialnote/base/editing/Style', + 'materialnote/base/editing/Typing', + 'materialnote/base/editing/Table', + 'materialnote/base/editing/Bullet' ], function ( - agent, key, func, list, dom, range, async, - History, Style, Typing, Table, Bullet + 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 (!event.isDefaultPrevented()) { - if (options.shortcuts) { - self.handleKeyMap(event); - } else { - self.preventDefaultEditableShortCuts(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, func.debounce(function () { - context.triggerEvent('change', $editable.html()); - }, 250)); - - $editor.on('focusin', function (event) { - context.triggerEvent('focusin', event); - }).on('focusout', function (event) { - context.triggerEvent('focusout', event); - }); - - if (!options.airMode) { - if (options.width) { - $editor.outerWidth(options.width); - } - if (options.height) { - $editable.outerHeight(options.height); - } - if (options.maxHeight) { - $editable.css('max-height', options.maxHeight); - } - if (options.minHeight) { - $editable.css('min-height', options.minHeight); - } - } - - history.recordUndo(); - }; + 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 (!event.isDefaultPrevented()) { + if (options.shortcuts) { + self.handleKeyMap(event); + } else { + self.preventDefaultEditableShortCuts(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, func.debounce(function () { + context.triggerEvent('change', $editable.html()); + }, 250)); + + $editor.on('focusin', function (event) { + context.triggerEvent('focusin', event); + }).on('focusout', function (event) { + context.triggerEvent('focusout', event); + }); + + if (!options.airMode) { + if (options.width) { + $editor.outerWidth(options.width); + } + if (options.height) { + $editable.outerHeight(options.height); + } + if (options.maxHeight) { + $editable.css('max-height', options.maxHeight); + } + if (options.minHeight) { + $editable.css('min-height', options.minHeight); + } + } + + history.recordUndo(); + }; - this.destroy = function () { - $editable.off(); - }; + 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(); - } - }; + 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(); + } + }; - this.preventDefaultEditableShortCuts = function (event) { - // B(Bold, 66) / I(Italic, 73) / U(Underline, 85) - if ((event.ctrlKey || event.metaKey) && - list.contains([66, 73, 85], event.keyCode)) { - event.preventDefault(); - } - }; + this.preventDefaultEditableShortCuts = function (event) { + // B(Bold, 66) / I(Italic, 73) / U(Underline, 85) + if ((event.ctrlKey || event.metaKey) && + list.contains([66, 73, 85], event.keyCode)) { + event.preventDefault(); + } + }; - /** - * create range - * @return {WrappedRange} - */ - this.createRange = function () { - this.focus(); - return range.create(editable); - }; + /** + * 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(); - } - }; + /** + * 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(); - } - }; + /** + * restoreRange + * + * restore lately range + */ + this.restoreRange = function () { + if (lastRange) { + lastRange.select(); + this.focus(); + } + }; - this.saveTarget = function (node) { - $editable.data('target', node); - }; + this.saveTarget = function (node) { + $editable.data('target', node); + }; - this.clearTarget = function () { - $editable.removeData('target'); - }; + this.clearTarget = function () { + $editable.removeData('target'); + }; - this.restoreTarget = function () { - return $editable.data('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); - }; + /** + * 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); - }; + /** + * 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); + /** + * 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(); + }; - /** - * redo - */ - this.redo = function () { - context.triggerEvent('before.command', $editable.html()); - history.redo(); - context.triggerEvent('change', $editable.html()); - }; - context.memo('help.redo', lang.help.redo); + /** + * after command + * @param {Boolean} isPreventTrigger + */ + var afterCommand = this.afterCommand = function (isPreventTrigger) { + history.recordUndo(); + if (!isPreventTrigger) { + context.triggerEvent('change', $editable.html()); + } + }; - /** - * before command - */ - var beforeCommand = this.beforeCommand = function () { - context.triggerEvent('before.command', $editable.html()); - // keep focus on editable before command execution - self.focus(); - }; + /* 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(); + }; + }; - /** - * after command - * @param {Boolean} isPreventTrigger - */ - var afterCommand = this.afterCommand = function (isPreventTrigger) { - history.recordUndo(); - if (!isPreventTrigger) { - context.triggerEvent('change', $editable.html()); - } - }; + /** + * insert paragraph + */ + this.insertParagraph = this.wrapCommand(function () { + typing.insertParagraph(editable); + }); + context.memo('help.insertParagraph', lang.help.insertParagraph); - /* 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 */ + this.insertOrderedList = this.wrapCommand(function () { + bullet.insertOrderedList(editable); + }); + context.memo('help.insertOrderedList', lang.help.insertOrderedList); - /** - * 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); + this.insertUnorderedList = this.wrapCommand(function () { + bullet.insertUnorderedList(editable); + }); + context.memo('help.insertUnorderedList', lang.help.insertUnorderedList); - /** - * 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); + this.indent = this.wrapCommand(function () { + bullet.indent(editable); + }); + context.memo('help.indent', lang.help.indent); - /** - * run given function between beforeCommand and afterCommand - */ - this.wrapCommand = function (fn) { - return function () { - beforeCommand(); - fn.apply(self, arguments); - afterCommand(); - }; - }; + 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.images, 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 = self.handleImageOptions($image, src.imageOptions); + + $image.show(); + range.create(editable).insertNode($image[0]); + range.createFromNodeAfter($image[0]).select(); + afterCommand(); + }).fail(function (e) { + context.triggerEvent('image.upload.error', e); + }); + }; - /** - * 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); + /** + * insertImages + * @param {File[]} files + */ + this.insertImages = function (files) { + var imageOptions = files.imageOptions; + + $.each(files.images, 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({images: dataURL, imageOptions: imageOptions}, filename); + }).fail(function () { + context.triggerEvent('image.upload.error'); + }); + } + }); + }; - /** - * 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())); - } + /** + * handle image options + * @param {$image} jQuery image node + * @param {Object} imageOptions + */ + this.handleImageOptions = function($image, imageOptions) { + if (imageOptions.responsive) { + $image.addClass('responsive-img'); + } + + return $image; + }; - $image.show(); - range.create(editable).insertNode($image[0]); - range.createFromNodeAfter($image[0]).select(); - afterCommand(); - }).fail(function (e) { - context.triggerEvent('image.upload.error', e); - }); - }; + /** + * insertImagesOrCallback + * @param {Object} files {images: Files[], imageOptions: Object{}} + */ + 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); + } + }; - /** - * 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'); - }); - } - }); - }; + /** + * insertNode + * insert node + * @param {Node} node + */ + this.insertNode = this.wrapCommand(function (node) { + var rng = this.createRange(); + rng.insertNode(node); + range.createFromNodeAfter(node).select(); + }); - /** - * 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); - } - }; + /** + * 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(); + }); - /** - * insertNode - * insert node - * @param {Node} node - */ - this.insertNode = this.wrapCommand(function (node) { - var rng = this.createRange(); - rng.insertNode(node); - range.createFromNodeAfter(node).select(); - }); + /** + * return selected plain text + * @return {String} text + */ + this.getSelectedText = function () { + var rng = this.createRange(); - /** - * 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(); - }); + // if range on anchor, expand range with anchor + if (rng.isOnAnchor()) { + rng = range.createFromNode(dom.ancestor(rng.sc, dom.isAnchor)); + } - /** - * 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(); - }; + 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(); - }); + /** + * 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, $target) { - var onApplyCustomStyle = context.options.callbacks.onApplyCustomStyle; - if (onApplyCustomStyle) { - onApplyCustomStyle.call(this, $target, context, this.onFormatBlock); - } else { - this.onFormatBlock(tagName); - } - }); - - this.onFormatBlock = function (tagName) { - // [workaround] for MSIE, IE need `<` - tagName = agent.isMSIE ? '<' + tagName + '>' : tagName; - document.execCommand('FormatBlock', false, tagName); - }; + /** + * formatBlock + * + * @param {String} tagName + */ + this.formatBlock = this.wrapCommand(function (tagName, $target) { + var onApplyCustomStyle = context.options.callbacks.onApplyCustomStyle; + if (onApplyCustomStyle) { + onApplyCustomStyle.call(this, $target, context, this.onFormatBlock); + } else { + this.onFormatBlock(tagName); + } + }); - this.formatPara = function () { - this.formatBlock('P'); - }; - context.memo('help.formatPara', lang.help.formatPara); + this.onFormatBlock = function (tagName) { + // [workaround] for MSIE, IE need `<` + tagName = agent.isMSIE ? '<' + tagName + '>' : tagName; + document.execCommand('FormatBlock', false, tagName); + }; - /* jshint ignore:start */ - for (var idx = 1; idx <= 6; idx ++) { - this['formatH' + idx] = function (idx) { - return function () { - this.formatBlock('H' + idx); + 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(); + } }; - }(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' + /** + * 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); - // [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(); - } - }; + /** + * remove bogus node and character + */ + this.removeBogus = function () { + var bogusNode = $editable.data(KEY_BOGUS); + if (!bogusNode) { + return; + } - /** - * 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); + var textNode = list.find(list.from(bogusNode.childNodes), dom.isText); - /** - * 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); - }; + var bogusCharIdx = textNode.nodeValue.indexOf(dom.ZERO_WIDTH_NBSP_CHAR); + if (bogusCharIdx !== -1) { + textNode.deleteData(bogusCharIdx, 1); + } - /** - * lineHeight - * @param {String} value - */ - this.lineHeight = this.wrapCommand(function (value) { - style.stylePara(this.createRange(), { - lineHeight: value - }); - }); + if (dom.isEmpty(bogusNode)) { + dom.remove(bogusNode); + } - /** - * 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(); - } - }; + $editable.removeData(KEY_BOGUS); + }; - /** - * 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; - - // handle spaced urls from input - if (typeof linkUrl === 'string') { - linkUrl = linkUrl.trim(); - } - - 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 + /** + * lineHeight + * @param {String} value + */ + this.lineHeight = this.wrapCommand(function (value) { + style.stylePara(this.createRange(), { + lineHeight: value + }); }); - } - - $.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(); + /** + * 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(); + } + }; - range.create( - startPoint.node, - startPoint.offset, - endPoint.node, - endPoint.offset - ).select(); - }); + /** + * 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; + + // handle spaced urls from input + if (typeof linkUrl === 'string') { + linkUrl = linkUrl.trim(); + } + + 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))); - var linkInfo = { - range: rng, - text: rng.toString(), - url: $anchor.length ? $anchor.attr('href') : '' - }; - - // Define isNewWindow when anchor exists. - if ($anchor.length) { - linkInfo.isNewWindow = $anchor.attr('target') === '_blank'; - } - - return linkInfo; - }; + /** + * 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))); + var linkInfo = { + range: rng, + text: rng.toString(), + url: $anchor.length ? $anchor.attr('href') : '' + }; + + // Define isNewWindow when anchor exists. + if ($anchor.length) { + linkInfo.isNewWindow = $anchor.attr('target') === '_blank'; + } + + return linkInfo; + }; - /** - * 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); } - }); + /** + * 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 (tableOptions) { - var dimension = tableOptions.dim.split('x'); - - var rng = this.createRange().deleteContents(); - rng.insertNode(table.createTable(dimension[0], dimension[1], tableOptions)); - }); - - /** - * @method addRow - * - * - */ - this.addRow = function (position) { - var rng = this.createRange($editable); - if (rng.isCollapsed() && rng.isOnCell()) { - beforeCommand(); - table.addRow(rng, position); - afterCommand(); - } - }; + /** + * insert Table + * + * @param {String} dimension of table (ex : "5x5") + */ + this.insertTable = this.wrapCommand(function (tableOptions) { + var dimension = tableOptions.dim.split('x'); - /** - * @method addCol - * - * - */ - this.addCol = function (position) { - var rng = this.createRange($editable); - if (rng.isCollapsed() && rng.isOnCell()) { - beforeCommand(); - table.addCol(rng, position); - afterCommand(); - } - }; + var rng = this.createRange().deleteContents(); + rng.insertNode(table.createTable(dimension[0], dimension[1], tableOptions)); + }); - /** - * @method deleteRow - * - * - */ - this.deleteRow = function () { - var rng = this.createRange($editable); - if (rng.isCollapsed() && rng.isOnCell()) { - beforeCommand(); - table.deleteRow(rng); - afterCommand(); - } - }; + /** + * @method addRow + * + * + */ + this.addRow = function (position) { + var rng = this.createRange($editable); + if (rng.isCollapsed() && rng.isOnCell()) { + beforeCommand(); + table.addRow(rng, position); + afterCommand(); + } + }; - /** - * @method deleteCol - * - * - */ - this.deleteCol = function () { - var rng = this.createRange($editable); - if (rng.isCollapsed() && rng.isOnCell()) { - beforeCommand(); - table.deleteCol(rng); - afterCommand(); - } - }; + /** + * @method addCol + * + * + */ + this.addCol = function (position) { + var rng = this.createRange($editable); + if (rng.isCollapsed() && rng.isOnCell()) { + beforeCommand(); + table.addCol(rng, position); + afterCommand(); + } + }; - /** - * @method deleteTable - * - * - */ - this.deleteTable = function () { - var rng = this.createRange($editable); - if (rng.isCollapsed() && rng.isOnCell()) { - beforeCommand(); - table.deleteTable(rng); - afterCommand(); - } - }; + /** + * @method deleteRow + * + * + */ + this.deleteRow = function () { + var rng = this.createRange($editable); + if (rng.isCollapsed() && rng.isOnCell()) { + beforeCommand(); + table.deleteRow(rng); + afterCommand(); + } + }; - /** - * float me - * - * @param {String} value - */ - this.floatMe = this.wrapCommand(function (value) { - var $target = $(this.restoreTarget()); - $target.css('float', value); - }); + /** + * @method deleteCol + * + * + */ + this.deleteCol = function () { + var rng = this.createRange($editable); + if (rng.isCollapsed() && rng.isOnCell()) { + beforeCommand(); + table.deleteCol(rng); + afterCommand(); + } + }; - /** - * resize overlay element - * @param {String} value - */ - this.resize = this.wrapCommand(function (value) { - var $target = $(this.restoreTarget()); - $target.css({ - width: value * 100 + '%', - height: '' - }); - }); + /** + * @method deleteTable + * + * + */ + this.deleteTable = function () { + var rng = this.createRange($editable); + if (rng.isCollapsed() && rng.isOnCell()) { + beforeCommand(); + table.deleteTable(rng); + afterCommand(); + } + }; - /** - * @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); - }; + /** + * float me + * + * @param {String} value + */ + this.floatMe = this.wrapCommand(function (value) { + var $target = $(this.restoreTarget()); + $target.css('float', value); + }); - /** - * remove media object - */ - this.removeMedia = this.wrapCommand(function () { - var $target = $(this.restoreTarget()).detach(); - context.triggerEvent('media.delete', $target, $editable); - }); + /** + * resize overlay element + * @param {String} value + */ + this.resize = this.wrapCommand(function (value) { + var $target = $(this.restoreTarget()); + $target.css({ + width: value * 100 + '%', + height: '' + }); + }); - /** - * returns whether editable area has focus or not. - */ - this.hasFocus = function () { - return $editable.is(':focus'); - }; + /** + * @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); + }; - /** - * 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(); - } - }; + /** + * remove media object + */ + this.removeMedia = this.wrapCommand(function () { + var $target = $(this.restoreTarget()).detach(); + context.triggerEvent('media.delete', $target, $editable); + }); - /** - * returns whether contents is empty or not. - * @return {Boolean} - */ - this.isEmpty = function () { - return dom.isEmpty($editable[0]) || dom.emptyPara === $editable.html(); - }; + /** + * returns whether editable area has focus or not. + */ + this.hasFocus = function () { + return $editable.is(':focus'); + }; - /** - * Removes all contents and restores the editable instance to an _emptyPara_. - */ - this.empty = function () { - context.invoke('code', dom.emptyPara); + /** + * 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}
added responsive option to imageDialog