<NME> dom.js
<BEF> define([
    'materialnote/base/core/func',
    'materialnote/base/core/list',
    'materialnote/base/core/agent'
], function (func, list, agent) {

    var NBSP_CHAR = String.fromCharCode(160);
    var ZERO_WIDTH_NBSP_CHAR = '\ufeff';

    /**
    * @class core.dom
    *
    * Dom functions
    *
    * @singleton
    * @alternateClassName dom
    */
    var dom = (function () {
        /**
        * @method isEditable
        *
        * returns whether node is `note-editable` or not.
        *
        * @param {Node} node
        * @return {Boolean}
        */
        var isEditable = function (node) {
            return node && $(node).hasClass('note-editable');
        };

        /**
        * @method isControlSizing
        *
        * returns whether node is `note-control-sizing` or not.
        *
        * @param {Node} node
        * @return {Boolean}
        */
        var isControlSizing = function (node) {
            return node && $(node).hasClass('note-control-sizing');
        };

        /**
        * @method makePredByNodeName
        *
        * returns predicate which judge whether nodeName is same
        *
        * @param {String} nodeName
        * @return {Function}
        */
        var makePredByNodeName = function (nodeName) {
            nodeName = nodeName.toUpperCase();
            return function (node) {
                if (node !== undefined && node.nodeName !== undefined) {
                    return node && node.nodeName.toUpperCase() === nodeName;
                }
                return false;
            };
        };

        /**
        * @method isText
        *
        *
        *
        * @param {Node} node
        * @return {Boolean} true if node's type is text(3)
        */
        var isText = function (node) {
            return node && node.nodeType === 3;
        };

        /**
        * @method isElement
        *
        *
        *
        * @param {Node} node
        * @return {Boolean} true if node's type is element(1)
        */
        var isElement = function (node) {
            return node && node.nodeType === 1;
        };

        /**
        * ex) br, col, embed, hr, img, input, ...
        * @see http://www.w3.org/html/wg/drafts/html/master/syntax.html#void-elements
        */
        var isVoid = function (node) {
            return node && /^BR|^IMG|^HR|^IFRAME|^BUTTON|^INPUT/.test(node.nodeName.toUpperCase());
        };

        var isPara = function (node) {
            if (isEditable(node)) {
                return false;
            }

            // Chrome(v31.0), FF(v25.0.1) use DIV for paragraph
            return node && /^DIV|^P|^LI|^H[1-7]/.test(node.nodeName.toUpperCase());
        };

        var isHeading = function (node) {
            return node && /^H[1-7]/.test(node.nodeName.toUpperCase());
        };

        var isPre = makePredByNodeName('PRE');

        var isLi = makePredByNodeName('LI');

        var isPurePara = function (node) {
            return isPara(node) && !isLi(node);
        };

        var isTable = makePredByNodeName('TABLE');

        var isData = makePredByNodeName('DATA');

        var isInline = function (node) {
            return !isBodyContainer(node) &&
            !isList(node) &&
            !isHr(node) &&
            !isPara(node) &&
            !isTable(node) &&
            !isBlockquote(node) &&
            !isData(node);
        };

        var isList = function (node) {
            return node && /^UL|^OL/.test(node.nodeName.toUpperCase());
        };

        var isHr = makePredByNodeName('HR');

        var isCell = function (node) {
            if (node !== undefined && node.nodeName !== undefined) {
                return node && /^TD|^TH/.test(node.nodeName.toUpperCase());
            }
            return false;
        };

        var isBlockquote = makePredByNodeName('BLOCKQUOTE');

        var isBodyContainer = function (node) {
            return isCell(node) || isBlockquote(node) || isEditable(node);
        };

        var isAnchor = makePredByNodeName('A');

        var isParaInline = function (node) {
            return isInline(node) && !!ancestor(node, isPara);
        };

        var isBodyInline = function (node) {
            return isInline(node) && !ancestor(node, isPara);
        };

        var isBody = makePredByNodeName('BODY');

        /**
        * returns whether nodeB is closest sibling of nodeA
        *
        * @param {Node} nodeA
        * @param {Node} nodeB
        * @return {Boolean}
        */
        var isClosestSibling = function (nodeA, nodeB) {
            return nodeA.nextSibling === nodeB ||
            nodeA.previousSibling === nodeB;
        };

        /**
        * returns array of closest siblings with node
        *
        * @param {Node} node
        * @param {function} [pred] - predicate function
        * @return {Node[]}
        */
        var withClosestSiblings = function (node, pred) {
            pred = pred || func.ok;

            var siblings = [];
            if (node.previousSibling && pred(node.previousSibling)) {
                siblings.push(node.previousSibling);
            }
            siblings.push(node);
            if (node.nextSibling && pred(node.nextSibling)) {
                siblings.push(node.nextSibling);
            }
            return siblings;
        };

        /**
        * blank HTML for cursor position
        * - [workaround] old IE only works with &nbsp;
        * - [workaround] IE11 and other browser works with bogus br
        */
        var blankHTML = agent.isMSIE && agent.browserVersion < 11 ? '&nbsp;' : '<br>';

        /**
        * @method nodeLength
        *
        * returns #text's text size or element's childNodes size
        *
        * @param {Node} node
        */
        var nodeLength = function (node) {
            if (isText(node)) {
                return node.nodeValue.length;
            }

            if (node) {
                return node.childNodes.length;
            }

            return 0;

        };

        /**
        * returns whether node is empty or not.
        *
        * @param {Node} node
        * @return {Boolean}
        */
        var isEmpty = function (node) {
            var len = nodeLength(node);

            if (len === 0) {
                return true;
            } else if (!isText(node) && len === 1 && node.innerHTML === blankHTML) {
                // ex) <p><br></p>, <span><br></span>
                return true;
            } else if (list.all(node.childNodes, isText) && node.innerHTML === '') {
                // ex) <p></p>, <span></span>
                return true;
            }

            return false;
        };

        /**
        * padding blankHTML if node is empty (for cursor position)
        */
        var paddingBlankHTML = function (node) {
            if (!isVoid(node) && !nodeLength(node)) {
                node.innerHTML = blankHTML;
            }
        };

        /**
        * find nearest ancestor predicate hit
        *
        * @param {Node} node
        * @param {Function} pred - predicate function
        */
        var ancestor = function (node, pred) {
            while (node) {
                if (pred(node)) { return node; }
                if (isEditable(node)) { break; }

                node = node.parentNode;
            }
            return null;
        };

        /**
        * find nearest ancestor only single child blood line and predicate hit
        *
        * @param {Node} node
        * @param {Function} pred - predicate function
        */
        var singleChildAncestor = function (node, pred) {
            node = node.parentNode;

            while (node) {
                if (nodeLength(node) !== 1) { break; }
                if (pred(node)) { return node; }
                if (isEditable(node)) { break; }

                node = node.parentNode;
            }
            return null;
        };

        /**
        * returns new array of ancestor nodes (until predicate hit).
        *
        * @param {Node} node
        * @param {Function} [optional] pred - predicate function
        */
        var listAncestor = function (node, pred) {
            pred = pred || func.fail;

            var ancestors = [];
            ancestor(node, function (el) {
                if (!isEditable(el)) {
                    ancestors.push(el);
                }

                return pred(el);
            });
            return ancestors;
        };

        /**
        * find farthest ancestor predicate hit
        */
        var lastAncestor = function (node, pred) {
            var ancestors = listAncestor(node);
            return list.last(ancestors.filter(pred));
        };

        /**
        * returns common ancestor node between two nodes.
        *
        * @param {Node} nodeA
        * @param {Node} nodeB
        */
        var commonAncestor = function (nodeA, nodeB) {
            var ancestors = listAncestor(nodeA);
            for (var n = nodeB; n; n = n.parentNode) {
                if ($.inArray(n, ancestors) > -1) { return n; }
            }
            return null; // difference document area
        };

        /**
        * listing all previous siblings (until predicate hit).
        *
        * @param {Node} node
        * @param {Function} [optional] pred - predicate function
        */
        var listPrev = function (node, pred) {
            pred = pred || func.fail;

            var nodes = [];
            while (node) {
                if (pred(node)) { break; }
                nodes.push(node);
                node = node.previousSibling;
            }
            return nodes;
        };

        /**
        * listing next siblings (until predicate hit).
        *
        * @param {Node} node
        * @param {Function} [pred] - predicate function
        */
        var listNext = function (node, pred) {
            pred = pred || func.fail;

            var nodes = [];
            while (node) {
                if (pred(node)) { break; }
                nodes.push(node);
                node = node.nextSibling;
            }
            return nodes;
        };

        /**
        * listing descendant nodes
        *
        * @param {Node} node
        * @param {Function} [pred] - predicate function
        */
        var listDescendant = function (node, pred) {
            var descendants = [];
            pred = pred || func.ok;

            // start DFS(depth first search) with node
            (function fnWalk(current) {
                if (node !== current && pred(current)) {
                    descendants.push(current);
                }
                for (var idx = 0, len = current.childNodes.length; idx < len; idx++) {
                    fnWalk(current.childNodes[idx]);
                }
            })(node);

            return descendants;
        };

        /**
        * wrap node with new tag.
        *
        * @param {Node} node
        * @param {Node} tagName of wrapper
        * @return {Node} - wrapper
        */
        var wrap = function (node, wrapperName) {
            var parent = node.parentNode;
            var wrapper = $('<' + wrapperName + '>')[0];

            parent.insertBefore(wrapper, node);
            wrapper.appendChild(node);

            return wrapper;
        };

        /**
        * insert node after preceding
        *
        * @param {Node} node
        * @param {Node} preceding - predicate function
        */
        var insertAfter = function (node, preceding) {
            var next = preceding.nextSibling, parent = preceding.parentNode;
            if (next) {
                parent.insertBefore(node, next);
            } else {
                parent.appendChild(node);
            }
            return node;
        };

        /**
        * append elements.
        *
        * @param {Node} node
        * @param {Collection} aChild
        */
        var appendChildNodes = function (node, aChild) {
            $.each(aChild, function (idx, child) {
                node.appendChild(child);
            });
            return node;
        };

        /**
        * returns whether boundaryPoint is left edge or not.
        *
        * @param {BoundaryPoint} point
        * @return {Boolean}
        */
        var isLeftEdgePoint = function (point) {
            return point.offset === 0;
        };

        /**
        * returns whether boundaryPoint is right edge or not.
        *
        * @param {BoundaryPoint} point
        * @return {Boolean}
        */
        var isRightEdgePoint = function (point) {
            return point.offset === nodeLength(point.node);
        };

        /**
        * returns whether boundaryPoint is edge or not.
        *
        * @param {BoundaryPoint} point
        * @return {Boolean}
        */
        var isEdgePoint = function (point) {
            return isLeftEdgePoint(point) || isRightEdgePoint(point);
        };

        /**
        * returns whether node is left edge of ancestor or not.
        *
        * @param {Node} node
        * @param {Node} ancestor
        * @return {Boolean}
        */
        var isLeftEdgeOf = function (node, ancestor) {
            while (node && node !== ancestor) {
                if (position(node) !== 0) {
                    return false;
                }
                node = node.parentNode;
            }

            return true;
        };

        /**
        * returns whether node is right edge of ancestor or not.
        *
        * @param {Node} node
        * @param {Node} ancestor
        * @return {Boolean}
        */
        var isRightEdgeOf = function (node, ancestor) {
            if (!ancestor) {
                return false;
            }
            while (node && node !== ancestor) {
                if (position(node) !== nodeLength(node.parentNode) - 1) {
                    return false;
                }
                node = node.parentNode;
            }

            return true;
        };

        /**
        * returns whether point is left edge of ancestor or not.
        * @param {BoundaryPoint} point
        * @param {Node} ancestor
        * @return {Boolean}
        */
        var isLeftEdgePointOf = function (point, ancestor) {
            return isLeftEdgePoint(point) && isLeftEdgeOf(point.node, ancestor);
        };

        /**
        * returns whether point is right edge of ancestor or not.
        * @param {BoundaryPoint} point
        * @param {Node} ancestor
        * @return {Boolean}
        */
        var isRightEdgePointOf = function (point, ancestor) {
            return isRightEdgePoint(point) && isRightEdgeOf(point.node, ancestor);
        };

        /**
        * returns offset from parent.
        *
        * @param {Node} node
        */
        var position = function (node) {
            var offset = 0;
            while ((node = node.previousSibling)) {
                offset += 1;
            }
            return offset;
        };

        var hasChildren = function (node) {
            return !!(node && node.childNodes && node.childNodes.length);
        };

        /**
        * returns previous boundaryPoint
        *
        * @param {BoundaryPoint} point
        * @param {Boolean} isSkipInnerOffset
        * @return {BoundaryPoint}
        */
        var prevPoint = function (point, isSkipInnerOffset) {
            var node, offset;

            if (point.offset === 0) {
                if (isEditable(point.node)) {
                    return null;
                }

                node = point.node.parentNode;
                offset = position(point.node);
            } else if (hasChildren(point.node)) {
                node = point.node.childNodes[point.offset - 1];
                offset = nodeLength(node);
            } else {
                node = point.node;
                offset = isSkipInnerOffset ? 0 : point.offset - 1;
            }

            return {
                node: node,
                offset: offset
            };
        };

        /**
        * returns next boundaryPoint
        *
        * @param {BoundaryPoint} point
        * @param {Boolean} isSkipInnerOffset
        * @return {BoundaryPoint}
        */
        var nextPoint = function (point, isSkipInnerOffset) {
            var node, offset;

            if (nodeLength(point.node) === point.offset) {
                if (isEditable(point.node)) {
                    return null;
                }

                node = point.node.parentNode;
                offset = position(point.node) + 1;
            } else if (hasChildren(point.node)) {
                node = point.node.childNodes[point.offset];
                offset = 0;
            } else {
                node = point.node;
                offset = isSkipInnerOffset ? nodeLength(point.node) : point.offset + 1;
            }

            return {
                node: node,
                offset: offset
            };
        };

        /**
        * returns whether pointA and pointB is same or not.
        *
        * @param {BoundaryPoint} pointA
        * @param {BoundaryPoint} pointB
        * @return {Boolean}
        */
        var isSamePoint = function (pointA, pointB) {
            return pointA.node === pointB.node && pointA.offset === pointB.offset;
        };

        /**
        * returns whether point is visible (can set cursor) or not.
        *
        * @param {BoundaryPoint} point
        * @return {Boolean}
        */
        var isVisiblePoint = function (point) {
            if (isText(point.node) || !hasChildren(point.node) || isEmpty(point.node)) {
                return true;
            }

            var leftNode = point.node.childNodes[point.offset - 1];
            var rightNode = point.node.childNodes[point.offset];
            if ((!leftNode || isVoid(leftNode)) && (!rightNode || isVoid(rightNode))) {
                return true;
            }

            return false;
        };

        /**
        * @method prevPointUtil
        *
        * @param {BoundaryPoint} point
        * @param {Function} pred
        * @return {BoundaryPoint}
        */
        var prevPointUntil = function (point, pred) {
            while (point) {
                if (pred(point)) {
                    return point;
                }

                point = prevPoint(point);
            }

            return null;
        };

        /**
        * @method nextPointUntil
        *
        * @param {BoundaryPoint} point
        * @param {Function} pred
        * @return {BoundaryPoint}
        */
        var nextPointUntil = function (point, pred) {
            while (point) {
                if (pred(point)) {
                    return point;
                }

                point = nextPoint(point);
            }

            return null;
        };

        /**
        * returns whether point has character or not.
        *
        * @param {Point} point
        * @return {Boolean}
        */
        var isCharPoint = function (point) {
            if (!isText(point.node)) {
                return false;
            }

            var ch = point.node.nodeValue.charAt(point.offset - 1);
            return ch && (ch !== ' ' && ch !== NBSP_CHAR);
        };

        /**
        * @method walkPoint
        *
        * @param {BoundaryPoint} startPoint
        * @param {BoundaryPoint} endPoint
        * @param {Function} handler
        * @param {Boolean} isSkipInnerOffset
        */
        var walkPoint = function (startPoint, endPoint, handler, isSkipInnerOffset) {
            var point = startPoint;

            while (point) {
                handler(point);

                if (isSamePoint(point, endPoint)) {
                    break;
                }

                var isSkipOffset = isSkipInnerOffset &&
                startPoint.node !== point.node &&
                endPoint.node !== point.node;
                point = nextPoint(point, isSkipOffset);
            }
        };

        /**
        * @method makeOffsetPath
        *
        * return offsetPath(array of offset) from ancestor
        *
        * @param {Node} ancestor - ancestor node
        * @param {Node} node
        */
        var makeOffsetPath = function (ancestor, node) {
            var ancestors = listAncestor(node, func.eq(ancestor));
            return ancestors.map(position).reverse();
        };

        /**
        * @method fromOffsetPath
        *
        * return element from offsetPath(array of offset)
        *
        * @param {Node} ancestor - ancestor node
        * @param {array} offsets - offsetPath
        */
        var fromOffsetPath = function (ancestor, offsets) {
            var current = ancestor;
            for (var i = 0, len = offsets.length; i < len; i++) {
                if (current.childNodes.length <= offsets[i]) {
                    current = current.childNodes[current.childNodes.length - 1];
                } else {
                    current = current.childNodes[offsets[i]];
                }
            }
            return current;
        };

        /**
        * @method splitNode
        *
        * split element or #text
        *
        * @param {BoundaryPoint} point
        * @param {Object} [options]
        * @param {Boolean} [options.isSkipPaddingBlankHTML] - default: false
        * @param {Boolean} [options.isNotSplitEdgePoint] - default: false
        * @return {Node} right node of boundaryPoint
        */
        var splitNode = function (point, options) {
            var isSkipPaddingBlankHTML = options && options.isSkipPaddingBlankHTML;
            var isNotSplitEdgePoint = options && options.isNotSplitEdgePoint;

            // edge case
            if (isEdgePoint(point) && (isText(point.node) || isNotSplitEdgePoint)) {
                if (isLeftEdgePoint(point)) {
                    return point.node;
                } else if (isRightEdgePoint(point)) {
                    return point.node.nextSibling;
                }
            }

            // split #text
            if (isText(point.node)) {
                return point.node.splitText(point.offset);
            } else {
                var childNode = point.node.childNodes[point.offset];
                var clone = insertAfter(point.node.cloneNode(false), point.node);
                appendChildNodes(clone, listNext(childNode));

                if (!isSkipPaddingBlankHTML) {
                    paddingBlankHTML(point.node);
                    paddingBlankHTML(clone);
                }

                return clone;
            }
        };

        /**
        * @method splitTree
        *
        * split tree by point
        *
        * @param {Node} root - split root
        * @param {BoundaryPoint} point
        * @param {Object} [options]
        * @param {Boolean} [options.isSkipPaddingBlankHTML] - default: false
        * @param {Boolean} [options.isNotSplitEdgePoint] - default: false
        * @return {Node} right node of boundaryPoint
        */
        var splitTree = function (root, point, options) {
            // ex) [#text, <span>, <p>]
            var ancestors = listAncestor(point.node, func.eq(root));

            if (!ancestors.length) {
                return null;
            } else if (ancestors.length === 1) {
                return splitNode(point, options);
            }

            return ancestors.reduce(function (node, parent) {
                if (node === point.node) {
                    node = splitNode(point, options);
                }

                return splitNode({
                    node: parent,
                    offset: node ? dom.position(node) : nodeLength(parent)
                }, options);
            });
        };

        /**
        * split point
        *
        * @param {Point} point
        * @param {Boolean} isInline
        * @return {Object}
        */
        var splitPoint = function (point, isInline) {
            // find splitRoot, container
            //  - inline: splitRoot is a child of paragraph
            //  - block: splitRoot is a child of bodyContainer
            var pred = isInline ? isPara : isBodyContainer;
            var ancestors = listAncestor(point.node, pred);
            var topAncestor = list.last(ancestors) || point.node;

            var splitRoot, container;
            if (pred(topAncestor)) {
                splitRoot = ancestors[ancestors.length - 2];
                container = topAncestor;
            } else {
                splitRoot = topAncestor;
                container = splitRoot.parentNode;
            }

            // if splitRoot is exists, split with splitTree
            var pivot = splitRoot && splitTree(splitRoot, point, {
                isSkipPaddingBlankHTML: isInline,
                isNotSplitEdgePoint: isInline
            });

            // if container is point.node, find pivot with point.offset
            if (!pivot && container === point.node) {
                pivot = point.node.childNodes[point.offset];
            }

            return {
                rightNode: pivot,
                container: container
            };
        };

        var create = function (nodeName) {
            return document.createElement(nodeName);
        };

        var createText = function (text) {
            return document.createTextNode(text);
        };

        /**
        * @method remove
        *
        * remove node, (isRemoveChild: remove child or not)
        *
        * @param {Node} node
        * @param {Boolean} isRemoveChild
        */
        var remove = function (node, isRemoveChild) {
            if (!node || !node.parentNode) { return; }
            if (node.removeNode) { return node.removeNode(isRemoveChild); }

            var parent = node.parentNode;
            if (!isRemoveChild) {
                var nodes = [];
                var i, len;
                for (i = 0, len = node.childNodes.length; i < len; i++) {
                    nodes.push(node.childNodes[i]);
                }

                for (i = 0, len = nodes.length; i < len; i++) {
                    parent.insertBefore(nodes[i], node);
                }
            }

            parent.removeChild(node);
        };

        /**
        * @method removeWhile
        *
        * @param {Node} node
        * @param {Function} pred
        */
        var removeWhile = function (node, pred) {
            while (node) {
                if (isEditable(node) || !pred(node)) {
                    break;
                }

                var parent = node.parentNode;
                remove(node);
                node = parent;
            }
        };

        /**
        * @method replace
        *
        * replace node with provided nodeName
        *
        * @param {Node} node
        * @param {String} nodeName
        * @return {Node} - new node
        */
        var replace = function (node, nodeName) {
            if (node.nodeName.toUpperCase() === nodeName.toUpperCase()) {
                return node;
            }

            var newNode = create(nodeName);

            if (node.style.cssText) {
                newNode.style.cssText = node.style.cssText;
            }

            appendChildNodes(newNode, list.from(node.childNodes));
            insertAfter(newNode, node);
            remove(node);

            return newNode;
        };

        var isTextarea = makePredByNodeName('TEXTAREA');

        /**
        * @param {jQuery} $node
        * @param {Boolean} [stripLinebreaks] - default: false
        */
        var value = function ($node, stripLinebreaks) {
            var val = isTextarea($node[0]) ? $node.val() : $node.html();
            if (stripLinebreaks) {
                return val.replace(/[\n\r]/g, '');
            }
            return val;
        };

        /**
        * @method html
        *
        * get the HTML contents of node
        *
        * @param {jQuery} $node
        * @param {Boolean} [isNewlineOnBlock]
        */
        var html = function ($node, isNewlineOnBlock) {
            var markup = value($node);

            if (isNewlineOnBlock) {
                var regexTag = /<(\/?)(\b(?!!)[^>\s]*)(.*?)(\s*\/?>)/g;
                markup = markup.replace(regexTag, function (match, endSlash, name) {
                    name = name.toUpperCase();
                    var isEndOfInlineContainer = /^DIV|^TD|^TH|^P|^LI|^H[1-7]/.test(name) &&
                    !!endSlash;
                    var isBlockNode = /^BLOCKQUOTE|^TABLE|^TBODY|^TR|^HR|^UL|^OL/.test(name);

                    return match + ((isEndOfInlineContainer || isBlockNode) ? '\n' : '');
                });
                markup = $.trim(markup);
            }

            return markup;
        };

        var posFromPlaceholder = function (placeholder) {
            let $placeholder = $(placeholder);
            let pos = $placeholder.offset();
            let height = $placeholder.outerHeight(true); // include margin

            return {
                left: pos.left,
                top: pos.top + height
            };
        };

        var attachEvents = function ($node, events) {
            Object.keys(events).forEach(function (key) {
                $node.on(key, events[key]);
            });
        };

        var detachEvents = function ($node, events) {
            Object.keys(events).forEach(function (key) {
                $node.off(key, events[key]);
            });
        };

        /**
        * @method isCustomStyleTag
        *
        * assert if a node contains a "note-styletag" class,
        * which implies that's a custom-made style tag node
        *
        * @param {Node} an HTML DOM node
        */
        var isCustomStyleTag = function (node) {
            return node && !dom.isText(node) && list.contains(node.classList, 'note-styletag');
        };

        return {
            /** @property {String} NBSP_CHAR */
            NBSP_CHAR: NBSP_CHAR,
            /** @property {String} ZERO_WIDTH_NBSP_CHAR */
            ZERO_WIDTH_NBSP_CHAR: ZERO_WIDTH_NBSP_CHAR,
            /** @property {String} blank */
            blank: blankHTML,
            /** @property {String} emptyPara */
            emptyPara: '<p>' + blankHTML + '</p>',
            emptyTableCell: '(table)',
            emptyTableHeaderCell: '(header)',
            makePredByNodeName: makePredByNodeName,
            isEditable: isEditable,
            isControlSizing: isControlSizing,
            isText: isText,
            isElement: isElement,
            isVoid: isVoid,
            isPara: isPara,
            isPurePara: isPurePara,
            isHeading: isHeading,
            isInline: isInline,
            isBlock: func.not(isInline),
            isBodyInline: isBodyInline,
            isBody: isBody,
            isParaInline: isParaInline,
            isPre: isPre,
            isList: isList,
            isTable: isTable,
            isData: isData,
            isCell: isCell,
            isBlockquote: isBlockquote,
            isBodyContainer: isBodyContainer,
            isAnchor: isAnchor,
            isDiv: makePredByNodeName('DIV'),
            isLi: isLi,
            isBR: makePredByNodeName('BR'),
            isSpan: makePredByNodeName('SPAN'),
            isB: makePredByNodeName('B'),
            isU: makePredByNodeName('U'),
            isS: makePredByNodeName('S'),
            isI: makePredByNodeName('I'),
            isImg: makePredByNodeName('IMG'),
            isTextarea: isTextarea,
            isEmpty: isEmpty,
            isEmptyAnchor: func.and(isAnchor, isEmpty),
            isClosestSibling: isClosestSibling,
            withClosestSiblings: withClosestSiblings,
            nodeLength: nodeLength,
            isLeftEdgePoint: isLeftEdgePoint,
            isRightEdgePoint: isRightEdgePoint,
            isEdgePoint: isEdgePoint,
            isLeftEdgeOf: isLeftEdgeOf,
            isRightEdgeOf: isRightEdgeOf,
            isLeftEdgePointOf: isLeftEdgePointOf,
            isRightEdgePointOf: isRightEdgePointOf,
            prevPoint: prevPoint,
            nextPoint: nextPoint,
            isSamePoint: isSamePoint,
            isVisiblePoint: isVisiblePoint,
            prevPointUntil: prevPointUntil,
            nextPointUntil: nextPointUntil,
            isCharPoint: isCharPoint,
            walkPoint: walkPoint,
            ancestor: ancestor,
            singleChildAncestor: singleChildAncestor,
            listAncestor: listAncestor,
            lastAncestor: lastAncestor,
            listNext: listNext,
            listPrev: listPrev,
            listDescendant: listDescendant,
            commonAncestor: commonAncestor,
            wrap: wrap,
            insertAfter: insertAfter,
            appendChildNodes: appendChildNodes,
            position: position,
            hasChildren: hasChildren,
            makeOffsetPath: makeOffsetPath,
            fromOffsetPath: fromOffsetPath,
            splitTree: splitTree,
            splitPoint: splitPoint,
            create: create,
            createText: createText,
            remove: remove,
            removeWhile: removeWhile,
            replace: replace,
            html: html,
            value: value,
            posFromPlaceholder: posFromPlaceholder,
            attachEvents: attachEvents,
            detachEvents: detachEvents,
            isCustomStyleTag: isCustomStyleTag
        };
    })();

    return dom;
});

<MSG> updating demo

<DFF> @@ -978,9 +978,9 @@ define([
         };
 
         var posFromPlaceholder = function (placeholder) {
-            let $placeholder = $(placeholder);
-            let pos = $placeholder.offset();
-            let height = $placeholder.outerHeight(true); // include margin
+            var $placeholder = $(placeholder);
+            var pos = $placeholder.offset();
+            var height = $placeholder.outerHeight(true); // include margin
 
             return {
                 left: pos.left,

updating demo