: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
var async = (function () {
* @method readFileAsDataURL
* read contents of file as representing URL
* @return {Promise} - then: dataUrl
var readFileAsDataURL = function (file) {
return $.Deferred(function (deferred) {
$.extend(new FileReader(), {
var dataURL = e.target.result;
deferred.resolve(dataURL);
* create `<image>` from url string
* @return {Promise} - then: $image
var createImage = function (url) {
return $.Deferred(function (deferred) {
$img.one('load', function () {
}).one('error abort', function () {
$img.off('load').detach();
}).appendTo(document.body).attr('src', url);
readFileAsDataURL: readFileAsDataURL,
var History = function ($editable) {
var stack = [], stackOffset = -1;
var editable = $editable[0];
var makeSnapshot = function () {
var rng = range.create();
var emptyBookmark = {s: {path: [], offset: 0}, e: {path: [], offset: 0}};
contents: $editable.html(),
bookmark: (rng ? rng.bookmark(editable) : emptyBookmark)
var applySnapshot = function (snapshot) {
if (snapshot.contents !== null) {
$editable.html(snapshot.contents);
if (snapshot.bookmark !== null) {
range.createFromBookmark(editable, snapshot.bookmark).select();
* Rewinds the history stack back to the first snapshot taken.
* Leaves the stack intact, so that "Redo" can still be used.
this.rewind = function () {
// Create snap shot if not yet recorded
if ($editable.html() !== stack[stackOffset].contents) {
// Return to the first available snapshot.
applySnapshot(stack[stackOffset]);
* Resets the history stack completely; reverting to an empty editor.
this.reset = function () {
// Restore stackOffset to its original value.
// Clear the editable area.
// Record our first snapshot (of nothing).
this.undo = function () {
// Create snap shot if not yet recorded
if ($editable.html() !== stack[stackOffset].contents) {
applySnapshot(stack[stackOffset]);
this.redo = function () {
if (stack.length - 1 > stackOffset) {
applySnapshot(stack[stackOffset]);
this.recordUndo = function () {
// Wash out stack after stackOffset
if (stack.length > stackOffset) {
stack = stack.slice(0, stackOffset);
// Create new snapshot and push it to the end
stack.push(makeSnapshot());
var Style = function () {
* [workaround] for old jQuery
* passing an array of style properties to .css()
* will result in an object of property-value pairs.
* (compability with version < 1.9)
* @param {Array} propertyNames - An array of one or more CSS properties.
var jQueryCSS = function ($obj, propertyNames) {
if (agent.jqueryVersion < 1.9) {
$.each(propertyNames, function (idx, propertyName) {
result[propertyName] = $obj.css(propertyName);
return $obj.css.call($obj, propertyNames);
* returns style object from node
this.fromNode = function ($node) {
var properties = ['font-family', 'font-size', 'text-align', 'list-style-type', 'line-height'];
var styleInfo = jQueryCSS($node, properties) || {};
styleInfo['font-size'] = parseInt(styleInfo['font-size'], 10);
* @param {WrappedRange} rng
* @param {Object} styleInfo
this.stylePara = function (rng, styleInfo) {
$.each(rng.nodes(dom.isPara, {
}), function (idx, para) {
* insert and returns styleNodes on range.
* @param {WrappedRange} rng
* @param {Object} [options] - options for styleNodes
* @param {String} [options.nodeName] - default: `SPAN`
* @param {Boolean} [options.expandClosestSibling] - default: `false`
* @param {Boolean} [options.onlyPartialContains] - default: `false`
this.styleNodes = function (rng, options) {
var nodeName = options && options.nodeName || 'SPAN';
var expandClosestSibling = !!(options && options.expandClosestSibling);
var onlyPartialContains = !!(options && options.onlyPartialContains);
return [rng.insertNode(dom.create(nodeName))];
var pred = dom.makePredByNodeName(nodeName);
var nodes = rng.nodes(dom.isText, {
return dom.singleChildAncestor(text, pred) || dom.wrap(text, nodeName);
if (expandClosestSibling) {
if (onlyPartialContains) {
var nodesInRange = rng.nodes();
// compose with partial contains predication
pred = func.and(pred, function (node) {
return list.contains(nodesInRange, node);
return nodes.map(function (node) {
var siblings = dom.withClosestSiblings(node, pred);
var head = list.head(siblings);
var tails = list.tail(siblings);
$.each(tails, function (idx, elem) {
dom.appendChildNodes(head, elem.childNodes);
return list.head(siblings);
* get current style on cursor
* @param {WrappedRange} rng
* @return {Object} - object contains style properties.
this.current = function (rng) {
var $cont = $(!dom.isElement(rng.sc) ? rng.sc.parentNode : rng.sc);
var styleInfo = this.fromNode($cont);
// document.queryCommandState for toggle state
// [workaround] prevent Firefox nsresult: "0x80004005 (NS_ERROR_FAILURE)"
styleInfo = $.extend(styleInfo, {
'font-bold': document.queryCommandState('bold') ? 'bold' : 'normal',
'font-italic': document.queryCommandState('italic') ? 'italic' : 'normal',
'font-underline': document.queryCommandState('underline') ? 'underline' : 'normal',
'font-subscript': document.queryCommandState('subscript') ? 'subscript' : 'normal',
'font-superscript': document.queryCommandState('superscript') ? 'superscript' : 'normal',
'font-strikethrough': document.queryCommandState('strikeThrough') ? 'strikethrough' : 'normal'
// list-style-type to list-style(unordered, ordered)
styleInfo['list-style'] = 'none';
var orderedTypes = ['circle', 'disc', 'disc-leading-zero', 'square'];
var isUnordered = $.inArray(styleInfo['list-style-type'], orderedTypes) > -1;
styleInfo['list-style'] = isUnordered ? 'unordered' : 'ordered';
var para = dom.ancestor(rng.sc, dom.isPara);
if (para && para.style['line-height']) {
styleInfo['line-height'] = para.style.lineHeight;
var lineHeight = parseInt(styleInfo['line-height'], 10) / parseInt(styleInfo['font-size'], 10);
styleInfo['line-height'] = lineHeight.toFixed(1);
styleInfo.anchor = rng.isOnAnchor() && dom.ancestor(rng.sc, dom.isAnchor);
styleInfo.ancestors = dom.listAncestor(rng.sc, dom.isEditable);
* @alternateClassName Bullet
var Bullet = function () {
* @method insertOrderedList
this.insertOrderedList = function () {
* @method insertUnorderedList
this.insertUnorderedList = function () {
this.indent = function () {
var rng = range.create().wrapBodyInlineWithPara();
var paras = rng.nodes(dom.isPara, { includeAncestor: true });
var clustereds = list.clusterBy(paras, func.peq2('parentNode'));
$.each(clustereds, function (idx, paras) {
var head = list.head(paras);
self.wrapList(paras, head.parentNode.nodeName);
$.each(paras, function (idx, para) {
$(para).css('marginLeft', function (idx, val) {
return (parseInt(val, 10) || 0) + 25;
this.outdent = function () {
var rng = range.create().wrapBodyInlineWithPara();
var paras = rng.nodes(dom.isPara, { includeAncestor: true });
var clustereds = list.clusterBy(paras, func.peq2('parentNode'));
$.each(clustereds, function (idx, paras) {
var head = list.head(paras);
self.releaseList([paras]);
$.each(paras, function (idx, para) {
$(para).css('marginLeft', function (idx, val) {
val = (parseInt(val, 10) || 0);
return val > 25 ? val - 25 : '';
* @param {String} listName - OL or UL
this.toggleList = function (listName) {
var rng = range.create().wrapBodyInlineWithPara();
var paras = rng.nodes(dom.isPara, { includeAncestor: true });
var bookmark = rng.paraBookmark(paras);
var clustereds = list.clusterBy(paras, func.peq2('parentNode'));
if (list.find(paras, dom.isPurePara)) {
$.each(clustereds, function (idx, paras) {
wrappedParas = wrappedParas.concat(self.wrapList(paras, listName));
// list to paragraph or change list style
var diffLists = rng.nodes(dom.isList, {
}).filter(function (listNode) {
return !$.nodeName(listNode, listName);
$.each(diffLists, function (idx, listNode) {
dom.replace(listNode, listName);
paras = this.releaseList(clustereds, true);
range.createFromParaBookmark(bookmark, paras).select();
* @param {String} listName
this.wrapList = function (paras, listName) {
var head = list.head(paras);
var last = list.last(paras);
var prevList = dom.isList(head.previousSibling) && head.previousSibling;
var nextList = dom.isList(last.nextSibling) && last.nextSibling;
var listNode = prevList || dom.insertAfter(dom.create(listName || 'UL'), last);
paras = paras.map(function (para) {
return dom.isPurePara(para) ? dom.replace(para, 'LI') : para;
// append to list(<ul>, <ol>)
dom.appendChildNodes(listNode, paras);
dom.appendChildNodes(listNode, list.from(nextList.childNodes));
* @param {Array[]} clustereds
* @param {Boolean} isEscapseToBody
this.releaseList = function (clustereds, isEscapseToBody) {
$.each(clustereds, function (idx, paras) {
var head = list.head(paras);
var last = list.last(paras);