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
801 lines (697 loc) • 23.5 kB
JavaScript
define([
'jquery',
'summernote/base/core/agent',
'summernote/base/core/func',
'summernote/base/core/list',
'summernote/base/core/dom'
], function ($, agent, func, list, dom) {
var range = (function () {
/**
* return boundaryPoint from TextRange, inspired by Andy Na's HuskyRange.js
*
* @param {TextRange} textRange
* @param {Boolean} isStart
* @return {BoundaryPoint}
*
* @see http://msdn.microsoft.com/en-us/library/ie/ms535872(v=vs.85).aspx
*/
var textRangeToPoint = function (textRange, isStart) {
var container = textRange.parentElement(), offset;
var tester = document.body.createTextRange(), prevContainer;
var childNodes = list.from(container.childNodes);
for (offset = 0; offset < childNodes.length; offset++) {
if (dom.isText(childNodes[offset])) {
continue;
}
tester.moveToElementText(childNodes[offset]);
if (tester.compareEndPoints('StartToStart', textRange) >= 0) {
break;
}
prevContainer = childNodes[offset];
}
if (offset !== 0 && dom.isText(childNodes[offset - 1])) {
var textRangeStart = document.body.createTextRange(), curTextNode = null;
textRangeStart.moveToElementText(prevContainer || container);
textRangeStart.collapse(!prevContainer);
curTextNode = prevContainer ? prevContainer.nextSibling : container.firstChild;
var pointTester = textRange.duplicate();
pointTester.setEndPoint('StartToStart', textRangeStart);
var textCount = pointTester.text.replace(/[\r\n]/g, '').length;
while (textCount > curTextNode.nodeValue.length && curTextNode.nextSibling) {
textCount -= curTextNode.nodeValue.length;
curTextNode = curTextNode.nextSibling;
}
/* jshint ignore:start */
var dummy = curTextNode.nodeValue; // enforce IE to re-reference curTextNode, hack
/* jshint ignore:end */
if (isStart && curTextNode.nextSibling && dom.isText(curTextNode.nextSibling) &&
textCount === curTextNode.nodeValue.length) {
textCount -= curTextNode.nodeValue.length;
curTextNode = curTextNode.nextSibling;
}
container = curTextNode;
offset = textCount;
}
return {
cont: container,
offset: offset
};
};
/**
* return TextRange from boundary point (inspired by google closure-library)
* @param {BoundaryPoint} point
* @return {TextRange}
*/
var pointToTextRange = function (point) {
var textRangeInfo = function (container, offset) {
var node, isCollapseToStart;
if (dom.isText(container)) {
var prevTextNodes = dom.listPrev(container, func.not(dom.isText));
var prevContainer = list.last(prevTextNodes).previousSibling;
node = prevContainer || container.parentNode;
offset += list.sum(list.tail(prevTextNodes), dom.nodeLength);
isCollapseToStart = !prevContainer;
} else {
node = container.childNodes[offset] || container;
if (dom.isText(node)) {
return textRangeInfo(node, 0);
}
offset = 0;
isCollapseToStart = false;
}
return {
node: node,
collapseToStart: isCollapseToStart,
offset: offset
};
};
var textRange = document.body.createTextRange();
var info = textRangeInfo(point.node, point.offset);
textRange.moveToElementText(info.node);
textRange.collapse(info.collapseToStart);
textRange.moveStart('character', info.offset);
return textRange;
};
/**
* Wrapped Range
*
* @constructor
* @param {Node} sc - start container
* @param {Number} so - start offset
* @param {Node} ec - end container
* @param {Number} eo - end offset
*/
var WrappedRange = function (sc, so, ec, eo) {
this.sc = sc;
this.so = so;
this.ec = ec;
this.eo = eo;
// nativeRange: get nativeRange from sc, so, ec, eo
var nativeRange = function () {
if (agent.isW3CRangeSupport) {
var w3cRange = document.createRange();
w3cRange.setStart(sc, so);
w3cRange.setEnd(ec, eo);
return w3cRange;
} else {
var textRange = pointToTextRange({
node: sc,
offset: so
});
textRange.setEndPoint('EndToEnd', pointToTextRange({
node: ec,
offset: eo
}));
return textRange;
}
};
this.getPoints = function () {
return {
sc: sc,
so: so,
ec: ec,
eo: eo
};
};
this.getStartPoint = function () {
return {
node: sc,
offset: so
};
};
this.getEndPoint = function () {
return {
node: ec,
offset: eo
};
};
/**
* select update visible range
*/
this.select = function () {
var nativeRng = nativeRange();
if (agent.isW3CRangeSupport) {
var selection = document.getSelection();
if (selection.rangeCount > 0) {
selection.removeAllRanges();
}
selection.addRange(nativeRng);
} else {
nativeRng.select();
}
return this;
};
/**
* Moves the scrollbar to start container(sc) of current range
*
* @return {WrappedRange}
*/
this.scrollIntoView = function (container) {
var height = $(container).height();
if (container.scrollTop + height < this.sc.offsetTop) {
container.scrollTop += Math.abs(container.scrollTop + height - this.sc.offsetTop);
}
return this;
};
/**
* @return {WrappedRange}
*/
this.normalize = function () {
/**
* @param {BoundaryPoint} point
* @param {Boolean} isLeftToRight
* @return {BoundaryPoint}
*/
var getVisiblePoint = function (point, isLeftToRight) {
if ((dom.isVisiblePoint(point) && !dom.isEdgePoint(point)) ||
(dom.isVisiblePoint(point) && dom.isRightEdgePoint(point) && !isLeftToRight) ||
(dom.isVisiblePoint(point) && dom.isLeftEdgePoint(point) && isLeftToRight) ||
(dom.isVisiblePoint(point) && dom.isBlock(point.node) && dom.isEmpty(point.node))) {
return point;
}
// point on block's edge
var block = dom.ancestor(point.node, dom.isBlock);
if (((dom.isLeftEdgePointOf(point, block) || dom.isVoid(dom.prevPoint(point).node)) && !isLeftToRight) ||
((dom.isRightEdgePointOf(point, block) || dom.isVoid(dom.nextPoint(point).node)) && isLeftToRight)) {
// returns point already on visible point
if (dom.isVisiblePoint(point)) {
return point;
}
// reverse direction
isLeftToRight = !isLeftToRight;
}
var nextPoint = isLeftToRight ? dom.nextPointUntil(dom.nextPoint(point), dom.isVisiblePoint) :
dom.prevPointUntil(dom.prevPoint(point), dom.isVisiblePoint);
return nextPoint || point;
};
var endPoint = getVisiblePoint(this.getEndPoint(), false);
var startPoint = this.isCollapsed() ? endPoint : getVisiblePoint(this.getStartPoint(), true);
return new WrappedRange(
startPoint.node,
startPoint.offset,
endPoint.node,
endPoint.offset
);
};
/**
* returns matched nodes on range
*
* @param {Function} [pred] - predicate function
* @param {Object} [options]
* @param {Boolean} [options.includeAncestor]
* @param {Boolean} [options.fullyContains]
* @return {Node[]}
*/
this.nodes = function (pred, options) {
pred = pred || func.ok;
var includeAncestor = options && options.includeAncestor;
var fullyContains = options && options.fullyContains;
// TODO compare points and sort
var startPoint = this.getStartPoint();
var endPoint = this.getEndPoint();
var nodes = [];
var leftEdgeNodes = [];
dom.walkPoint(startPoint, endPoint, function (point) {
if (dom.isEditable(point.node)) {
return;
}
var node;
if (fullyContains) {
if (dom.isLeftEdgePoint(point)) {
leftEdgeNodes.push(point.node);
}
if (dom.isRightEdgePoint(point) && list.contains(leftEdgeNodes, point.node)) {
node = point.node;
}
} else if (includeAncestor) {
node = dom.ancestor(point.node, pred);
} else {
node = point.node;
}
if (node && pred(node)) {
nodes.push(node);
}
}, true);
return list.unique(nodes);
};
/**
* returns commonAncestor of range
* @return {Element} - commonAncestor
*/
this.commonAncestor = function () {
return dom.commonAncestor(sc, ec);
};
/**
* returns expanded range by pred
*
* @param {Function} pred - predicate function
* @return {WrappedRange}
*/
this.expand = function (pred) {
var startAncestor = dom.ancestor(sc, pred);
var endAncestor = dom.ancestor(ec, pred);
if (!startAncestor && !endAncestor) {
return new WrappedRange(sc, so, ec, eo);
}
var boundaryPoints = this.getPoints();
if (startAncestor) {
boundaryPoints.sc = startAncestor;
boundaryPoints.so = 0;
}
if (endAncestor) {
boundaryPoints.ec = endAncestor;
boundaryPoints.eo = dom.nodeLength(endAncestor);
}
return new WrappedRange(
boundaryPoints.sc,
boundaryPoints.so,
boundaryPoints.ec,
boundaryPoints.eo
);
};
/**
* @param {Boolean} isCollapseToStart
* @return {WrappedRange}
*/
this.collapse = function (isCollapseToStart) {
if (isCollapseToStart) {
return new WrappedRange(sc, so, sc, so);
} else {
return new WrappedRange(ec, eo, ec, eo);
}
};
/**
* splitText on range
*/
this.splitText = function () {
var isSameContainer = sc === ec;
var boundaryPoints = this.getPoints();
if (dom.isText(ec) && !dom.isEdgePoint(this.getEndPoint())) {
ec.splitText(eo);
}
if (dom.isText(sc) && !dom.isEdgePoint(this.getStartPoint())) {
boundaryPoints.sc = sc.splitText(so);
boundaryPoints.so = 0;
if (isSameContainer) {
boundaryPoints.ec = boundaryPoints.sc;
boundaryPoints.eo = eo - so;
}
}
return new WrappedRange(
boundaryPoints.sc,
boundaryPoints.so,
boundaryPoints.ec,
boundaryPoints.eo
);
};
/**
* delete contents on range
* @return {WrappedRange}
*/
this.deleteContents = function () {
if (this.isCollapsed()) {
return this;
}
var rng = this.splitText();
var nodes = rng.nodes(null, {
fullyContains: true
});
// find new cursor point
var point = dom.prevPointUntil(rng.getStartPoint(), function (point) {
return !list.contains(nodes, point.node);
});
var emptyParents = [];
$.each(nodes, function (idx, node) {
// find empty parents
var parent = node.parentNode;
if (point.node !== parent && dom.nodeLength(parent) === 1) {
emptyParents.push(parent);
}
dom.remove(node, false);
});
// remove empty parents
$.each(emptyParents, function (idx, node) {
dom.remove(node, false);
});
return new WrappedRange(
point.node,
point.offset,
point.node,
point.offset
).normalize();
};
/**
* makeIsOn: return isOn(pred) function
*/
var makeIsOn = function (pred) {
return function () {
var ancestor = dom.ancestor(sc, pred);
return !!ancestor && (ancestor === dom.ancestor(ec, pred));
};
};
// isOnEditable: judge whether range is on editable or not
this.isOnEditable = makeIsOn(dom.isEditable);
// isOnList: judge whether range is on list node or not
this.isOnList = makeIsOn(dom.isList);
// isOnAnchor: judge whether range is on anchor node or not
this.isOnAnchor = makeIsOn(dom.isAnchor);
// isOnAnchor: judge whether range is on cell node or not
this.isOnCell = makeIsOn(dom.isCell);
/**
* @param {Function} pred
* @return {Boolean}
*/
this.isLeftEdgeOf = function (pred) {
if (!dom.isLeftEdgePoint(this.getStartPoint())) {
return false;
}
var node = dom.ancestor(this.sc, pred);
return node && dom.isLeftEdgeOf(this.sc, node);
};
/**
* returns whether range was collapsed or not
*/
this.isCollapsed = function () {
return sc === ec && so === eo;
};
/**
* wrap inline nodes which children of body with paragraph
*
* @return {WrappedRange}
*/
this.wrapBodyInlineWithPara = function () {
if (dom.isBodyContainer(sc) && dom.isEmpty(sc)) {
sc.innerHTML = dom.emptyPara;
return new WrappedRange(sc.firstChild, 0, sc.firstChild, 0);
}
/**
* [workaround] firefox often create range on not visible point. so normalize here.
* - firefox: |<p>text</p>|
* - chrome: <p>|text|</p>
*/
var rng = this.normalize();
if (dom.isParaInline(sc) || dom.isPara(sc)) {
return rng;
}
// find inline top ancestor
var topAncestor;
if (dom.isInline(rng.sc)) {
var ancestors = dom.listAncestor(rng.sc, func.not(dom.isInline));
topAncestor = list.last(ancestors);
if (!dom.isInline(topAncestor)) {
topAncestor = ancestors[ancestors.length - 2] || rng.sc.childNodes[rng.so];
}
} else {
topAncestor = rng.sc.childNodes[rng.so > 0 ? rng.so - 1 : 0];
}
// siblings not in paragraph
var inlineSiblings = dom.listPrev(topAncestor, dom.isParaInline).reverse();
inlineSiblings = inlineSiblings.concat(dom.listNext(topAncestor.nextSibling, dom.isParaInline));
// wrap with paragraph
if (inlineSiblings.length) {
var para = dom.wrap(list.head(inlineSiblings), 'p');
dom.appendChildNodes(para, list.tail(inlineSiblings));
}
return this.normalize();
};
/**
* insert node at current cursor
*
* @param {Node} node
* @return {Node}
*/
this.insertNode = function (node) {
var rng = this.wrapBodyInlineWithPara().deleteContents();
var info = dom.splitPoint(rng.getStartPoint(), dom.isInline(node));
if (info.rightNode) {
info.rightNode.parentNode.insertBefore(node, info.rightNode);
} else {
info.container.appendChild(node);
}
return node;
};
/**
* insert html at current cursor
*/
this.pasteHTML = function (markup) {
var contentsContainer = $('<div></div>').html(markup)[0];
var childNodes = list.from(contentsContainer.childNodes);
var rng = this.wrapBodyInlineWithPara().deleteContents();
return childNodes.reverse().map(function (childNode) {
return rng.insertNode(childNode);
}).reverse();
};
/**
* returns text in range
*
* @return {String}
*/
this.toString = function () {
var nativeRng = nativeRange();
return agent.isW3CRangeSupport ? nativeRng.toString() : nativeRng.text;
};
/**
* returns range for word before cursor
*
* @param {Boolean} [findAfter] - find after cursor, default: false
* @return {WrappedRange}
*/
this.getWordRange = function (findAfter) {
var endPoint = this.getEndPoint();
if (!dom.isCharPoint(endPoint)) {
return this;
}
var startPoint = dom.prevPointUntil(endPoint, function (point) {
return !dom.isCharPoint(point);
});
if (findAfter) {
endPoint = dom.nextPointUntil(endPoint, function (point) {
return !dom.isCharPoint(point);
});
}
return new WrappedRange(
startPoint.node,
startPoint.offset,
endPoint.node,
endPoint.offset
);
};
/**
* create offsetPath bookmark
*
* @param {Node} editable
*/
this.bookmark = function (editable) {
return {
s: {
path: dom.makeOffsetPath(editable, sc),
offset: so
},
e: {
path: dom.makeOffsetPath(editable, ec),
offset: eo
}
};
};
/**
* create offsetPath bookmark base on paragraph
*
* @param {Node[]} paras
*/
this.paraBookmark = function (paras) {
return {
s: {
path: list.tail(dom.makeOffsetPath(list.head(paras), sc)),
offset: so
},
e: {
path: list.tail(dom.makeOffsetPath(list.last(paras), ec)),
offset: eo
}
};
};
/**
* getClientRects
* @return {Rect[]}
*/
this.getClientRects = function () {
var nativeRng = nativeRange();
return nativeRng.getClientRects();
};
};
/**
* @class core.range
*
* Data structure
* * BoundaryPoint: a point of dom tree
* * BoundaryPoints: two boundaryPoints corresponding to the start and the end of the Range
*
* See to http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Position
*
* @singleton
* @alternateClassName range
*/
return {
/**
* create Range Object From arguments or Browser Selection
*
* @param {Node} sc - start container
* @param {Number} so - start offset
* @param {Node} ec - end container
* @param {Number} eo - end offset
* @return {WrappedRange}
*/
create: function (sc, so, ec, eo) {
if (arguments.length === 4) {
return new WrappedRange(sc, so, ec, eo);
} else if (arguments.length === 2) { //collapsed
ec = sc;
eo = so;
return new WrappedRange(sc, so, ec, eo);
} else {
var wrappedRange = this.createFromSelection();
if (!wrappedRange && arguments.length === 1) {
wrappedRange = this.createFromNode(arguments[0]);
return wrappedRange.collapse(dom.emptyPara === arguments[0].innerHTML);
}
return wrappedRange;
}
},
createFromSelection: function () {
var sc, so, ec, eo;
if (agent.isW3CRangeSupport) {
var selection = document.getSelection();
if (!selection || selection.rangeCount === 0) {
return null;
} else if (dom.isBody(selection.anchorNode)) {
// Firefox: returns entire body as range on initialization.
// We won't never need it.
return null;
}
var nativeRng = selection.getRangeAt(0);
sc = nativeRng.startContainer;
so = nativeRng.startOffset;
ec = nativeRng.endContainer;
eo = nativeRng.endOffset;
} else { // IE8: TextRange
var textRange = document.selection.createRange();
var textRangeEnd = textRange.duplicate();
textRangeEnd.collapse(false);
var textRangeStart = textRange;
textRangeStart.collapse(true);
var startPoint = textRangeToPoint(textRangeStart, true),
endPoint = textRangeToPoint(textRangeEnd, false);
// same visible point case: range was collapsed.
if (dom.isText(startPoint.node) && dom.isLeftEdgePoint(startPoint) &&
dom.isTextNode(endPoint.node) && dom.isRightEdgePoint(endPoint) &&
endPoint.node.nextSibling === startPoint.node) {
startPoint = endPoint;
}
sc = startPoint.cont;
so = startPoint.offset;
ec = endPoint.cont;
eo = endPoint.offset;
}
return new WrappedRange(sc, so, ec, eo);
},
/**
* @method
*
* create WrappedRange from node
*
* @param {Node} node
* @return {WrappedRange}
*/
createFromNode: function (node) {
var sc = node;
var so = 0;
var ec = node;
var eo = dom.nodeLength(ec);
// browsers can't target a picture or void node
if (dom.isVoid(sc)) {
so = dom.listPrev(sc).length - 1;
sc = sc.parentNode;
}
if (dom.isBR(ec)) {
eo = dom.listPrev(ec).length - 1;
ec = ec.parentNode;
} else if (dom.isVoid(ec)) {
eo = dom.listPrev(ec).length;
ec = ec.parentNode;
}
return this.create(sc, so, ec, eo);
},
/**
* create WrappedRange from node after position
*
* @param {Node} node
* @return {WrappedRange}
*/
createFromNodeBefore: function (node) {
return this.createFromNode(node).collapse(true);
},
/**
* create WrappedRange from node after position
*
* @param {Node} node
* @return {WrappedRange}
*/
createFromNodeAfter: function (node) {
return this.createFromNode(node).collapse();
},
/**
* @method
*
* create WrappedRange from bookmark
*
* @param {Node} editable
* @param {Object} bookmark
* @return {WrappedRange}
*/
createFromBookmark: function (editable, bookmark) {
var sc = dom.fromOffsetPath(editable, bookmark.s.path);
var so = bookmark.s.offset;
var ec = dom.fromOffsetPath(editable, bookmark.e.path);
var eo = bookmark.e.offset;
return new WrappedRange(sc, so, ec, eo);
},
/**
* @method
*
* create WrappedRange from paraBookmark
*
* @param {Object} bookmark
* @param {Node[]} paras
* @return {WrappedRange}
*/
createFromParaBookmark: function (bookmark, paras) {
var so = bookmark.s.offset;
var eo = bookmark.e.offset;
var sc = dom.fromOffsetPath(list.head(paras), bookmark.s.path);
var ec = dom.fromOffsetPath(list.last(paras), bookmark.e.path);
return new WrappedRange(sc, so, ec, eo);
}
};
})();
return range;
});