alpaca
Version:
Alpaca provides the easiest and fastest way to generate interactive forms for the web and mobile devices. It runs simply as HTML5 or more elaborately using Bootstrap, jQuery Mobile or jQuery UI. Alpaca uses Handlebars to process JSON schema and provide
1,898 lines (1,657 loc) • 200 kB
JavaScript
/**
* Super simple wysiwyg editor v0.8.1
* http://summernote.org/
*
* summernote.js
* Copyright 2013-2015 Alan Hong. and other contributors
* summernote may be freely distributed under the MIT license./
*
* Date: 2016-02-15T18:35Z
*/
(function (factory) {
/* global define */
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['jquery'], factory);
} else if (typeof module === 'object' && module.exports) {
// Node/CommonJS
module.exports = factory(require('jquery'));
} else {
// Browser globals
factory(window.jQuery);
}
}(function ($) {
'use strict';
/**
* @class core.func
*
* func utils (for high-order func's arg)
*
* @singleton
* @alternateClassName func
*/
var func = (function () {
var eq = function (itemA) {
return function (itemB) {
return itemA === itemB;
};
};
var eq2 = function (itemA, itemB) {
return itemA === itemB;
};
var peq2 = function (propName) {
return function (itemA, itemB) {
return itemA[propName] === itemB[propName];
};
};
var ok = function () {
return true;
};
var fail = function () {
return false;
};
var not = function (f) {
return function () {
return !f.apply(f, arguments);
};
};
var and = function (fA, fB) {
return function (item) {
return fA(item) && fB(item);
};
};
var self = function (a) {
return a;
};
var invoke = function (obj, method) {
return function () {
return obj[method].apply(obj, arguments);
};
};
var idCounter = 0;
/**
* generate a globally-unique id
*
* @param {String} [prefix]
*/
var uniqueId = function (prefix) {
var id = ++idCounter + '';
return prefix ? prefix + id : id;
};
/**
* returns bnd (bounds) from rect
*
* - IE Compatibility Issue: http://goo.gl/sRLOAo
* - Scroll Issue: http://goo.gl/sNjUc
*
* @param {Rect} rect
* @return {Object} bounds
* @return {Number} bounds.top
* @return {Number} bounds.left
* @return {Number} bounds.width
* @return {Number} bounds.height
*/
var rect2bnd = function (rect) {
var $document = $(document);
return {
top: rect.top + $document.scrollTop(),
left: rect.left + $document.scrollLeft(),
width: rect.right - rect.left,
height: rect.bottom - rect.top
};
};
/**
* returns a copy of the object where the keys have become the values and the values the keys.
* @param {Object} obj
* @return {Object}
*/
var invertObject = function (obj) {
var inverted = {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
inverted[obj[key]] = key;
}
}
return inverted;
};
/**
* @param {String} namespace
* @param {String} [prefix]
* @return {String}
*/
var namespaceToCamel = function (namespace, prefix) {
prefix = prefix || '';
return prefix + namespace.split('.').map(function (name) {
return name.substring(0, 1).toUpperCase() + name.substring(1);
}).join('');
};
return {
eq: eq,
eq2: eq2,
peq2: peq2,
ok: ok,
fail: fail,
self: self,
not: not,
and: and,
invoke: invoke,
uniqueId: uniqueId,
rect2bnd: rect2bnd,
invertObject: invertObject,
namespaceToCamel: namespaceToCamel
};
})();
/**
* @class core.list
*
* list utils
*
* @singleton
* @alternateClassName list
*/
var list = (function () {
/**
* returns the first item of an array.
*
* @param {Array} array
*/
var head = function (array) {
return array[0];
};
/**
* returns the last item of an array.
*
* @param {Array} array
*/
var last = function (array) {
return array[array.length - 1];
};
/**
* returns everything but the last entry of the array.
*
* @param {Array} array
*/
var initial = function (array) {
return array.slice(0, array.length - 1);
};
/**
* returns the rest of the items in an array.
*
* @param {Array} array
*/
var tail = function (array) {
return array.slice(1);
};
/**
* returns item of array
*/
var find = function (array, pred) {
for (var idx = 0, len = array.length; idx < len; idx ++) {
var item = array[idx];
if (pred(item)) {
return item;
}
}
};
/**
* returns true if all of the values in the array pass the predicate truth test.
*/
var all = function (array, pred) {
for (var idx = 0, len = array.length; idx < len; idx ++) {
if (!pred(array[idx])) {
return false;
}
}
return true;
};
/**
* returns index of item
*/
var indexOf = function (array, item) {
return $.inArray(item, array);
};
/**
* returns true if the value is present in the list.
*/
var contains = function (array, item) {
return indexOf(array, item) !== -1;
};
/**
* get sum from a list
*
* @param {Array} array - array
* @param {Function} fn - iterator
*/
var sum = function (array, fn) {
fn = fn || func.self;
return array.reduce(function (memo, v) {
return memo + fn(v);
}, 0);
};
/**
* returns a copy of the collection with array type.
* @param {Collection} collection - collection eg) node.childNodes, ...
*/
var from = function (collection) {
var result = [], idx = -1, length = collection.length;
while (++idx < length) {
result[idx] = collection[idx];
}
return result;
};
/**
* returns whether list is empty or not
*/
var isEmpty = function (array) {
return !array || !array.length;
};
/**
* cluster elements by predicate function.
*
* @param {Array} array - array
* @param {Function} fn - predicate function for cluster rule
* @param {Array[]}
*/
var clusterBy = function (array, fn) {
if (!array.length) { return []; }
var aTail = tail(array);
return aTail.reduce(function (memo, v) {
var aLast = last(memo);
if (fn(last(aLast), v)) {
aLast[aLast.length] = v;
} else {
memo[memo.length] = [v];
}
return memo;
}, [[head(array)]]);
};
/**
* returns a copy of the array with all falsy values removed
*
* @param {Array} array - array
* @param {Function} fn - predicate function for cluster rule
*/
var compact = function (array) {
var aResult = [];
for (var idx = 0, len = array.length; idx < len; idx ++) {
if (array[idx]) { aResult.push(array[idx]); }
}
return aResult;
};
/**
* produces a duplicate-free version of the array
*
* @param {Array} array
*/
var unique = function (array) {
var results = [];
for (var idx = 0, len = array.length; idx < len; idx ++) {
if (!contains(results, array[idx])) {
results.push(array[idx]);
}
}
return results;
};
/**
* returns next item.
* @param {Array} array
*/
var next = function (array, item) {
var idx = indexOf(array, item);
if (idx === -1) { return null; }
return array[idx + 1];
};
/**
* returns prev item.
* @param {Array} array
*/
var prev = function (array, item) {
var idx = indexOf(array, item);
if (idx === -1) { return null; }
return array[idx - 1];
};
return { head: head, last: last, initial: initial, tail: tail,
prev: prev, next: next, find: find, contains: contains,
all: all, sum: sum, from: from, isEmpty: isEmpty,
clusterBy: clusterBy, compact: compact, unique: unique };
})();
var isSupportAmd = typeof define === 'function' && define.amd;
/**
* returns whether font is installed or not.
*
* @param {String} fontName
* @return {Boolean}
*/
var isFontInstalled = function (fontName) {
var testFontName = fontName === 'Comic Sans MS' ? 'Courier New' : 'Comic Sans MS';
var $tester = $('<div>').css({
position: 'absolute',
left: '-9999px',
top: '-9999px',
fontSize: '200px'
}).text('mmmmmmmmmwwwwwww').appendTo(document.body);
var originalWidth = $tester.css('fontFamily', testFontName).width();
var width = $tester.css('fontFamily', fontName + ',' + testFontName).width();
$tester.remove();
return originalWidth !== width;
};
var userAgent = navigator.userAgent;
var isMSIE = /MSIE|Trident/i.test(userAgent);
var browserVersion;
if (isMSIE) {
var matches = /MSIE (\d+[.]\d+)/.exec(userAgent);
if (matches) {
browserVersion = parseFloat(matches[1]);
}
matches = /Trident\/.*rv:([0-9]{1,}[\.0-9]{0,})/.exec(userAgent);
if (matches) {
browserVersion = parseFloat(matches[1]);
}
}
var isEdge = /Edge\/\d+/.test(userAgent);
var hasCodeMirror = !!window.CodeMirror;
if (!hasCodeMirror && isSupportAmd && require) {
if (require.hasOwnProperty('resolve')) {
try {
// If CodeMirror can't be resolved, `require.resolve` will throw an
// exception and `hasCodeMirror` won't be set to `true`.
require.resolve('codemirror');
hasCodeMirror = true;
} catch (e) {
hasCodeMirror = false;
}
} else if (require.hasOwnProperty('specified')) {
hasCodeMirror = require.specified('codemirror');
}
}
/**
* @class core.agent
*
* Object which check platform and agent
*
* @singleton
* @alternateClassName agent
*/
var agent = {
isMac: navigator.appVersion.indexOf('Mac') > -1,
isMSIE: isMSIE,
isEdge: isEdge,
isFF: !isEdge && /firefox/i.test(userAgent),
isPhantom: /PhantomJS/i.test(userAgent),
isWebkit: !isEdge && /webkit/i.test(userAgent),
isChrome: !isEdge && /chrome/i.test(userAgent),
isSafari: !isEdge && /safari/i.test(userAgent),
browserVersion: browserVersion,
jqueryVersion: parseFloat($.fn.jquery),
isSupportAmd: isSupportAmd,
hasCodeMirror: hasCodeMirror,
isFontInstalled: isFontInstalled,
isW3CRangeSupport: !!document.createRange
};
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) {
return node && node.nodeName.toUpperCase() === nodeName;
};
};
/**
* @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/.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 isInline = function (node) {
return !isBodyContainer(node) &&
!isList(node) &&
!isHr(node) &&
!isPara(node) &&
!isTable(node) &&
!isBlockquote(node);
};
var isList = function (node) {
return node && /^UL|^OL/.test(node.nodeName.toUpperCase());
};
var isHr = makePredByNodeName('HR');
var isCell = function (node) {
return node && /^TD|^TH/.test(node.nodeName.toUpperCase());
};
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
* - [workaround] IE11 and other browser works with bogus br
*/
var blankHTML = agent.isMSIE && agent.browserVersion < 11 ? ' ' : '<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;
}
return node.childNodes.length;
};
/**
* 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) {
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) {
var $placeholder = $(placeholder);
var pos = $placeholder.offset();
var 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]);
});
};
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>',
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,
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
};
})();
/**
* @param {jQuery} $note
* @param {Object} options
* @return {Context}
*/
var Context = function ($note, options) {
var self = this;
var ui = $.summernote.ui;
this.memos = {};
this.modules = {};
this.layoutInfo = {};
this.options = options;
/**
* create layout and initialize modules and other resources
*/
this.initialize = function () {
this.layoutInfo = ui.createLayout($note, options);
this._initialize();
$note.hide();
return this;
};
/**
* destroy modules and other resources and remove layout
*/
this.destroy = function () {
this._destroy();
$note.removeData('summernote');
ui.removeLayout($note, this.layoutInfo);
};
/**
* destory modules and other resources and initialize it again
*/
this.reset = function () {
var disabled = self.isDisabled();
this.code(dom.emptyPara);
this._destroy();
this._initialize();
if (disabled) {
self.disable();
}
};
this._initialize = function () {
// add optional buttons
var buttons = $.extend({}, this.options.buttons);
Object.keys(buttons).forEach(function (key) {
self.memo('button.' + key, buttons[key]);
});
var modules = $.extend({}, this.options.modules, $.summernote.plugins || {});
// add and initialize modules
Object.keys(modules).forEach(function (key) {
self.module(key, modules[key], true);
});
Object.keys(this.modules).forEach(function (key) {
self.initializeModule(key);
});
};
this._destroy = function () {
// destroy modules with reversed order
Object.keys(this.modules).reverse().forEach(function (key) {
self.removeModule(key);
});
Object.keys(this.memos).forEach(function (key) {
self.removeMemo(key);
});
};
this.code = function (html) {
var isActivated = this.invoke('codeview.isActivated');
if (html === undefined) {
this.invoke('codeview.sync');
return isActivated ? this.layoutInfo.codable.val() : this.layoutInfo.editable.html();
} else {
if (isActivated) {
this.layoutInfo.codable.val(html);
} else {
this.layoutInfo.editable.html(html);
}
$note.val(html);
this.triggerEvent('change', html);
}
};
this.isDisabled = function () {
return this.layoutInfo.editable.attr('contenteditable') === 'false';
};
this.enable = function () {
this.layoutInfo.editable.attr('contenteditable', true);
this.invoke('toolbar.activate', true);
};
this.disable = function () {
// close codeview if codeview is opend
if (this.invoke('codeview.isActivated')) {
this.invoke('codeview.deactivate');
}
this.layoutInfo.editable.attr('contenteditable', false);
this.invoke('toolbar.deactivate', true);
};
this.triggerEvent = function () {
var namespace = list.head(arguments);
var args = list.tail(list.from(arguments));
var callback = this.options.callbacks[func.namespaceToCamel(namespace, 'on')];
if (callback) {
callback.apply($note[0], args);
}
$note.trigger('summernote.' + namespace, args);
};
this.initializeModule = function (key) {
var module = this.modules[key];
module.shouldInitialize = module.shouldInitialize || func.ok;
if (!module.shouldInitialize()) {
return;
}
// initialize module
if (module.initialize) {
module.initialize();
}
// attach events
if (module.events) {
dom.attachEvents($note, module.events);
}
};
this.module = function (key, ModuleClass, withoutIntialize) {
if (arguments.length === 1) {
return this.modules[key];
}
this.modules[key] = new ModuleClass(this);
if (!withoutIntialize) {
this.initializeModule(key);
}
};
this.removeModule = function (key) {
var module = this.modules[key];
if (module.shouldInitialize()) {
if (module.events) {
dom.detachEvents($note, module.events);
}
if (module.destroy) {
module.destroy();
}
}
delete this.modules[key];
};
this.memo = function (key, obj) {
if (arguments.length === 1) {
return this.memos[key];
}
this.memos[key] = obj;
};
this.removeMemo = function (key) {
if (this.memos[key] && this.memos[key].destroy) {
this.memos[key].destroy();
}
delete this.memos[key];
};
this.createInvokeHandler = function (namespace, value) {
return function (event) {
event.preventDefault();
self.invoke(namespace, value || $(event.target).closest('[data-value]').data('value'));
};
};
this.invoke = function () {
var namespace = list.head(arguments);
var args = list.tail(list.from(arguments));
var splits = namespace.split('.');
var hasSeparator = splits.length > 1;
var moduleName = hasSeparator && list.head(splits);
var methodName = hasSeparator ? list.last(splits) : list.head(splits);
var module = this.modules[moduleName || 'editor'];
if (!moduleName && this[methodName]) {
return this[methodName].apply(this, args);
} else if (module && module[methodName] && module.shouldInitialize()) {
return module[methodName].apply(module, args);
}
};
return this.initialize();
};
$.fn.extend({
/**
* Summernote API
*
* @param {Object|String}
* @return {this}
*/
summernote: function () {
var type = $.type(list.head(arguments));
var isExternalAPICalled = type === 'string';
var hasInitOptions = type === 'object';
var options = hasInitOptions ? list.head(arguments) : {};
options = $.extend({}, $.summernote.options, options);
options.langInfo = $.extend(true, {}, $.summernote.lang['en-US'], $.summernote.lang[options.lang]);
this.each(function (idx, note) {
var $note = $(note);
if (!$note.data('summernote')) {
var context = new Context($note, options);
$note.data('summernote', context);
$note.data('summernote').triggerEvent('init', context.layoutInfo);
}
});
var $note = this.first();
if ($note.length) {
var context = $note.data('summernote');
if (isExternalAPICalled) {
return context.invoke.apply(context, list.from(arguments));
} else if (options.focus) {
context.invoke('editor.focus');
}
}
return this;
}
});
var Renderer = function (markup, children, options, callback) {
this.render = function ($parent) {
var $node = $(markup);
if (options && options.contents) {
$node.html(options.contents);
}
if (options && options.className) {
$node.addClass(options.className);
}
if (options && options.data) {
$.each(options.data, function (k, v) {
$node.attr('data-' + k, v);
});
}
if (options && options.click) {
$node.on('click', options.click);
}
if (children) {
var $container = $node.find('.note-children-container');
children.forEach(function (child) {
child.render($container.length ? $container : $node);
});
}
if (callback) {
callback($node, options);
}
if (options && options.callback) {
options.callback($node);
}
if ($parent) {
$parent.append($node);
}
return $node;
};
};
var renderer = {
create: function (markup, callback) {
return function () {
var children = $.isArray(arguments[0]) ? arguments[0] : [];
var options = typeof arguments[1] === 'object' ? arguments[1] : arguments[0];
if (options && options.children) {
children = options.children;
}
return new Renderer(markup, children, options, callback);
};
}
};
var editor = renderer.create('<div class="note-editor note-frame panel panel-default"/>');
var toolbar = renderer.create('<div class="note-toolbar panel-heading"/>');
var editingArea = renderer.create('<div class="note-editing-area"/>');
var codable = renderer.create('<textarea class="note-codable"/>');
var editable = renderer.create('<div class="note-editable panel-body" contentEditable="true"/>');
var statusbar = renderer.create([
'<div class="note-statusbar">',
' <div class="note-resizebar">',
' <div class="note-icon-bar"/>',
' <div class="note-icon-bar"/>',
' <div class="note-icon-bar"/>',
' </div>',
'</div>'
].join(''));
var airEditor = renderer.create('<div class="note-editor"/>');
var airEditable = renderer.create('<div class="note-editable" contentEditable="true"/>');
var buttonGroup = renderer.create('<div class="note-btn-group btn-group">');
var button = renderer.create('<button type="button" class="note-btn btn btn-default btn-sm">', function ($node, options) {
if (options && options.tooltip) {
$node.attr({
title: options.tooltip
}).tooltip({
container: 'body',
trigger: 'hover',
placement: 'bottom'
});
}
});
var dropdown = renderer.create('<div class="dropdown-menu">', function ($node, options) {
var markup = $.isArray(options.items) ? options.items.map(function (item) {
var value = (typeof item === 'string') ? item : (item.value || '');
var content = options.template ? options.template(item) : item;
return '<li><a href="#" data-value="' + value + '">' + content + '</a></li>';
}).join('') : options.items;
$node.html(markup);
});
var dropdownCheck = renderer.create('<div class="dropdown-menu note-check">', function ($node, options) {
var markup = $.isArray(options.items) ? options.items.map(function (item) {
var value = (typeof item === 'string') ? item : (item.value || '');
var content = options.template ? options.template(item) : item;
return '<li><a href="#" data-value="' + value + '">' + icon(options.checkClassName) + ' ' + content + '</a></li>';
}).join('') : options.items;
$node.html(markup);
});
var palette = renderer.create('<div class="note-color-palette"/>', function ($node, options) {
var contents = [];
for (var row = 0, rowSize = options.colors.length; row < rowSize; row++) {
var eventName = options.eventName;
var colors = options.colors[row];
var buttons = [];
for (var col = 0, colSize = colors.length; col < colSize; col++) {
var color = colors[col];
buttons.push([
'<button type="button" class="note-color-btn"',
'style="background-color:', color, '" ',
'data-event="', eventName, '" ',
'data-value="', color, '" ',
'title="', color, '" ',
'data-toggle="button" tabindex="-1"></button>'
].join(''));
}
contents.push('<div class="note-color-row">' + buttons.join('') + '</div>');
}
$node.html(contents.join(''));
$node.find('.note-color-btn').tooltip({
container: 'body',
trigger: 'hover',
placement: 'bottom'
});
});
var dialog = renderer.create('<div class="modal" aria-hidden="false" tabindex="-1"/>', function ($node, options) {
if (options.fade) {
$node.addClass('fade');
}