<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 card
        *
        * @param {String} backColor
        * @return {DOM}
        */
        this.insertCard = function (card) {
            range.create(editable).insertNode($('<div class="card ' + card.backColor + ' ' + card.foreColor + '"><div class="card-content">' +
            card.title + '</div></div>')[0]);
        };

        /**
        * 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);
            });
        };

        /**
        * 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');
                    });
                }
            });
        };

        /**
        * 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;
        };

        /**
        * 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);
            }
        };

        /**
        * 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() {
            if ($anchor.length) {
                linkInfo.isNewWindow = $anchor.attr('target') === '_blank';
            }
            console.log(linkInfo);

            return linkInfo;
        };
                text: rng.toString(),
        /**
        * returns image info
        *
        */
        this.getImageInfo = function(imageDom) {
            let $image = $(imageDom);

        /**
        * returns image info
        *
        * @param {DOM element} imageDom
        * @return {Boolean} imageInfo.responsive
        * @return {Number} (or undefined) imageInfo.size
        * @return {String} (or undefined) imageInfo.float
        */
        this.getImageInfo = function(imageDom) {
            let $image = $(imageDom);
            let styles = context.getInlineStyles($image);
            let imageInfo = {
                responsive: $image.hasClass('responsive-img') ? true : false
            };

            // handle inline style attribute of the image
            for (let key in styles) {
                if (styles.hasOwnProperty(key)) {
                    let value = styles[key];

                    // handle styles
                    switch(key) {
                        case 'width':
                            switch(value) {
                                case '100%':
                                imageInfo.size = 100;
                                break;

                                case '50%':
                                imageInfo.size = 50;
                                break;

                                case '25%':
                                imageInfo.size = 25;
                                break;
                            }
                        break;

                        default:
                            imageInfo[key] = value;
                    }
                }
            }
            return imageInfo;
        };

        /**
        * returns table info
        *
        * @param {DOM element} imageDom
        * @return {Boolean} imageInfo.responsive
        * @return {Number} (or undefined) imageInfo.size
        * @return {String} (or undefined) imageInfo.float
        */
        this.getTableInfo = function(tableDom) {
            let $table = $(tableDom).closest('table');
            let tableInfo = {
                bordered: $table.hasClass('bordered') ? true : false,
                striped: $table.hasClass('striped') ? true : false,
                highlighted: $table.hasClass('highlight') ? true : false,
                responsive: $table.hasClass('responsive-table') ? true : false,
                centered: $table.hasClass('centered') ? true : false
            };

            return tableInfo;
        };

        /**
        * 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) {
            let foreColor = colorInfo.foreColor;
            let 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));
        });

        /**
        * add or remove materialize's classes to table
        */
        this.updateTable = function(option) {
            let rng = this.createRange($editable);
            let cell = dom.ancestor(rng.commonAncestor(), dom.isCell);
            let $table = $(cell).closest('table');

            beforeCommand();
            $table.toggleClass(option);

            afterCommand(true);
            context.triggerEvent('change', $table[0]);
        };

        /**
        * @method addRow
        *
        *
        */
        this.addRow = function (position) {
            let rng = this.createRange($editable);
            let cell = dom.ancestor(rng.commonAncestor(), dom.isCell);

            if (rng.isCollapsed() && rng.isOnCell()) {
                beforeCommand();
                table.addRow(rng, position);
                afterCommand(true);
                context.triggerEvent('change', cell);
            }
        };

        /**
        * @method addCol
        *
        *
        */
        this.addCol = function (position) {
            let rng = this.createRange($editable);
            let cell = dom.ancestor(rng.commonAncestor(), dom.isCell);

            if (rng.isCollapsed() && rng.isOnCell()) {
                beforeCommand();
                table.addCol(rng, position);
                afterCommand(true);
                context.triggerEvent('change', cell);
            }
        };

        /**
        * @method deleteRow
        *
        *
        *
        * @param {String} value
        */
        this.floatMe = function (value) {
            var $target = $(this.restoreTarget());

            beforeCommand();
            currentTargetIndex++;
            // set newTarget to prev or next row, same column
            let newTarget = currentRow.prev('tr').children('td:nth-child(' + currentTargetIndex + ')');

            if (newTarget.length === 0) {
                newTarget = currentRow.next('tr').children('td:nth-child(' + currentTargetIndex + ')');
            }
        * resize overlay element
        * @param {String} value
        */

        this.resize = function (value) {
            var $target = $(this.restoreTarget());

            beforeCommand();
                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);
            context.triggerEvent('change', $target[0]);
        };

        /**
        * @param {Position} pos
        * @param {jQuery} $target - target element
            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 btn and handler for responsivity on imagePopover

<DFF> @@ -671,7 +671,6 @@ define([
             if ($anchor.length) {
                 linkInfo.isNewWindow = $anchor.attr('target') === '_blank';
             }
-            console.log(linkInfo);
 
             return linkInfo;
         };
@@ -679,6 +678,10 @@ define([
         /**
         * returns image info
         *
+        * @param {DOM element} imageDom
+        * @return {Boolean} imageInfo.responsive
+        * @return {Number} (or undefined) imageInfo.size
+        * @return {String} (or undefined) imageInfo.float
         */
         this.getImageInfo = function(imageDom) {
             let $image = $(imageDom);
@@ -833,7 +836,7 @@ define([
         *
         * @param {String} value
         */
-        this.floatMe = function (value) {
+        this.floatMe = function(value) {
             var $target = $(this.restoreTarget());
 
             beforeCommand();
@@ -847,8 +850,7 @@ define([
         * resize overlay element
         * @param {String} value
         */
-
-        this.resize = function (value) {
+        this.resize = function(value) {
             var $target = $(this.restoreTarget());
 
             beforeCommand();
@@ -861,6 +863,19 @@ define([
             context.triggerEvent('change', $target[0]);
         };
 
+        /**
+        * add or remove materialize's responsive class to image
+        */
+        this.responsivize = function() {
+            var $target = $(this.restoreTarget());
+
+            beforeCommand();
+            $target.toggleClass('responsive-img');
+
+            afterCommand(true);
+            context.triggerEvent('change', $target[0]);
+        };
+
         /**
         * @param {Position} pos
         * @param {jQuery} $target - target element

added btn and handler for responsivity on imagePopover